ucan-wg / ts-ucan Goto Github PK
View Code? Open in Web Editor NEWAuth tokens for a distributed, user-controlled world
Home Page: https://www.npmjs.com/package/ucans
License: Apache License 2.0
Auth tokens for a distributed, user-controlled world
Home Page: https://www.npmjs.com/package/ucans
License: Apache License 2.0
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
.
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 %
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
I may be misunderstanding something and if so it is probably a good idea to update language to communicate this more clearly.
ts-ucan
does not provide a raw decoding function.
Tools that wish to inspect a UCAN in its raw form are unable to do so without some level of parsing/validation.
Provide a raw decoding function.
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:
.
separatorsNaming and/or namespacing will be an important consideration for the last point. A few possibilities:
ucan.unsafeDecode
ucan.unutterableFoulDecode
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.
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:
We will still need to read in arbitrary JWTs, but the more canonicalized UCANs that exist, the better a world we live in 😉
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
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)
What have been the ucan-wg's thoughts on leveraging WebAssembly? I'd imagine it could help:
Disclaimer - I work on the Polywrap project, a WebAssembly toolchain + runtime that could offer a solution here https://polywrap.io/
ts-ucan
does not allow nbf
and nnc
to be null
.
ts-ucan
reports an invalid payload when either of these fields are null
.
Allow the fields to be null
when validating payloads.
The fields are checked here:
Lines 122 to 123 in 978dfd2
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)
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.
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.
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 👀
Specifically,
Lines 4 to 10 in bfbdbda
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
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.
The multicodec spec says that the public key DID for NIST ECDSA should be the compressed public key (33 bytes) instead of the full (64 byte) public key.
The spec is live!
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.
delegateCapability
on the Builder API passes the issuer
as both audience
and requiredIssuer
to Store.findWithCapability.
This works if the UCAN you are looking for has the capability by parenthood but not if the capability came from delegation
https://github.com/ucan-wg/ts-ucan/blob/main/packages/core/src/builder.ts#L224
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?
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
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)
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.
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?
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.
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
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 ✨
Thoughts? I'd be happy to help.
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.
New format RSA PK multicodec made its way over to the did:key
spec: https://github.com/w3c-ccg/did-method-key/blob/main/index.html#L362-L381
We should write a github CI (?) file that runs the test suite we have in tests/
for every PR.
I was trying to use this library in observable snippet and run into issue, it seems that something is confusing skypack CDN resulting in broken library. Here is the link to an example https://observablehq.com/@protocol/ucan-skypack-problem
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
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'
}
}
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:
Lines 63 to 125 in bfbdbda
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:
Lines 204 to 210 in 6a75d56
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)
// ...
}
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.
Current definition of capability is too restrictive
Specifically by demanding all values to be strings it makes it inconvenient to capture numeric restrictions like upload quota, or number of uploads etc..
It is possible to encode those as strings, however it probably makes more sense to loosen that restriction.
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.
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?
I was wondering what the API is for validating ucans. In my little demo app i used something ucan.isValid
-- https://github.com/nichoth/ucan-demo/blob/main/src/index.js#L296 -- but I don't see it documented anywhere now.
There is this great example for creating a token -- https://github.com/ucan-wg/ts-ucan#example . it would be great to have something similar for validating the ucan chain.
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
.
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
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.
The usual case. Compare only if the capabilities are in the same namespace/semantics.
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.
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:
dag-ucan also lower cases all can
s 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
dag-ucan omits fct
field if it an empty array https://github.com/ipld/js-dag-ucan/blob/main/src/formatter.js#L58
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.
The docs show
const keypair = await ucan.keypair.create(ucan.KeyType.Edwards)
But when I try that, https://github.com/nichoth/ucan-demo/blob/3f79989fe2f3d71ae807ec57a78ad1dd72022d77/src/index.js#L10, it says ucan.KeyType
is undefined.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.