Skip to content

***Notice: This repository is no longer maintained.***UCAN.storage spec and packages

Notifications You must be signed in to change notification settings

nftstorage/ucan.storage

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

46 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

UCAN.Storage

❗️ This repo is likely no longer going to be maintained. NFT.Storage is in the process of being migrated to a natively UCAN-based API. If you have any questions, please reach out on the #nft-storage channel on IPFS Discord.

This project defines a specification for delegated authorization of decentralized storage services using UCAN, or User Controlled Authorization Networks.

UCAN.Storage was designed to be used with NFT.Storage and Web3.Storage, which both provide storage services backed by the Filecoin network, with content retrieval via the InterPlanetary File System (IPFS).

This repository also contains the reference implementation of the spec, in the form of a JavaScript package called ucan-storage. See the Use cases section below to see how it can be used, along with some example code for each use case.

How it works

Authorization using UCANs works differently than many other authorization schemes, so it's worth taking a moment to understand the terms and concepts involved.

First, we have a storage service, for example, NFT.Storage. Normally (without UCAN), when a user signs up for an account at NFT.Storage, the service will give them an API token to authenticate and authorize uploads. This is standard Web 2 auth, and it works great, but it has some limitations, especially if you want to use NFT.Storage to provide services for your own end users.

For example, if you're building an NFT marketplace and want users to upload art to NFT.Storage before minting, you can't put your API token into the source code for your web application without exposing it to the world. Since your API token includes more permissions than just uploading, like deleting uploads from your account, this isn't a great solution. You could work around this by running a proxy server that hides your token from your users and attaches it to storage requests, but this means you need to relay all traffic through your server and pay for bandwidth costs.

UCAN provides a way for the storage service to issue a special kind of authorization token called a UCAN token. UCAN tokens can be used to derive "child" UCAN tokens, which can have a subset of the permissions encoded in the "parent" UCAN.

Participants in the UCAN auth flow are identified by a keypair, which is a private signing key with a corresponding public verification key. Each user or service involved in the flow will have their own keypair. The public key for each user or service is encoded into a Decentralized Identity Document (DID), using the did:key method, which encodes the public key into a compact string of the form did:key:<encoded-public-key>. These DID strings are used to identify each of the participants in the UCAN flow.

UCAN tokens are standard JSON Web Tokens (JWTs) with some additional properties that allow the kind of delegated chains of authority we've been describing. The UCAN data structure specifies some required properties of the JWT payload, some of which, like iss and aud are standard fields in the JWT spec.

The iss or "issuer" field contains the public key that issued the token, encoded as a DID. The public key can be used to verify the token's signature, which must be created with the corresponding private signing key.

The aud or "audience" field contains the public key that should receive the token.

The nbf or "not before" and exp or "expiry" fields contain Unix timestamps that can be used to control the time window during which the token should be considered valid.

The prf or "proof" field contains the "chain of proofs" that validates the delegated chain of authority.

The att or "attenuations" field specifies the permissions that the token should grant to the bearer. These are described in the Storage capabilities section below, and in greater detail in the UCAN.Storage spec.

To illustrate the authorization flow, let's walk through an example using NFT.Storage as the storage service and an NFT marketplace that wants to allow their users to upload to NFT.Storage.

First, the marketplace will generate a keypair and register their DID with the storage service, then ask the service to issue them a root token. The root token is a UCAN token that encodes all the permissions that the marketplace account is allowed to perform. The iss field of the root token will be the DID for the storage service, and the aud field will be the DID for the marketplace.

When an end-user logs into the marketplace and wants to upload to NFT.Storage, the marketplace can use their root token to create a user token. This time, the iss field contains the DID for the marketplace, since they are the one issuing the token, and the aud field contains the DID of the end user. The prf or "proof" field of the user token will contain a copy of the marketplace's root token, to verify that they actually have the permissions they're attempting to delegate. The root token is safe to share with the end-user, because it cannot be "redeemed" for storage services without the marketplace's private key.

When issuing the user token, the marketplace can choose to grant all the permissions that they have access to via the root token, or they can grant a subset of the permissions. The marketplace can also set an expiration time for the user tokens, so that a lost or compromised token will eventually expire. See Storage capabilities below for more about the permissions available.

Once a marketplace end-user has a user token, they'll create one last token, a request token that authorizes their upload request to the NFT.Storage service. The request token is generated by the user, most likely in the browser with JavaScript, and it must include a signature from their private key.

The request token has the end-user's DID in the iss field, with the DID for the NFT.Storage service in the aud field. The prf field contains a copy of the user token that was issued by the marketplace, which in turn has the root token in its own prf field.

The request token is attached to the upload to NFT.Storage, which validates the chain of proofs encoded in the token and confirms the cryptographic identity of each participant by checking the token signatures. If the token is valid and the permissions encoded in the request token are sufficient to carry out the request, it will succeed.

Storage capabilities

UCAN tokens encode permissions as a set of "capabilities," which are objects describing actions that the token holder can perform upon some "resource."

UCAN.Storage supports the storage capability, which represents access to operations over storage resources (e.g., uploading a file to NFT.Storage).

A capability object looks like this:

{
  "with": "storage://did:key:<user-public-key>",
  "can": "upload/*"
}

The with field specifies the resource pointer, which in the case of UCAN.Storage is a string that includes the DID of the user to whom the token was issued. A storage resource pointer issued by a service that supports UCAN.Storage will always begin with the storage:// prefix, followed by the DID that the token was issued to (the "audience" of the token).

When deriving child tokens for a new user, you will probably want to restrict that user's access to a sub-path of your storage. A simple way to do this is to append the DID of the new user to the resource path, with / characters separating the DID strings. For example, if your DID is did:key:marketplace, the token issued by the storage service would have the resource storage://did:key:marketplace. You can then issue a token to a user with the DID did:key:user-1 and a resource path of storage://did:key:marketplace/did:key:user-1.

The can field specifies what action the token holder is authorized to perform. UCAN.Storage currently supports the upload/* action, which allows uploading content. Additional actions with restrictions on permitted content may be added in a future release.

See the UCAN.Storage spec for more details.

Installation and usage

You can install the ucan-storage package with your favorite JS dependency manager, e.g.:

npm install ucan-storage

The main exports are the build and validate methods, as well as the [KeyPair class][typedoc-keypair] used to manage signing keys.

import { build, validate } from 'ucan-storage/ucan-storage'
import { KeyPair } from 'ucan-storage/keypair'

Using the ucan-storage command-line tool

The ucan-storage package includes a command-line interface (CLI) that can help create keypairs and to create and validate UCAN tokens.

The ucan-storage command will be available if you install the package globally with npm install -g ucan-storage. Alternatively, you can run without installing globally by using npx:

npx ucan-storage --help

The first time you run a command with npx, you'll get a prompt like this:

Need to install the following packages:
  ucan-storage
Ok to proceed? (y)

Answer y to make the command available to npx.

Use cases

The ucan-storage JavaScript package supports the creation and verification of UCAN tokens, including the ability to create the "proof chains" that enable delgated authorization.

This README will walk through some common scenarios, to illustrate the main features of the ucan-storage library. For more details, see the API reference documentation.

Generating a keypair

To participate in the UCAN flow (both as a service, and as an end-user), you'll need a keypair.

To generate a keypair using ucan-storage, use the static KeyPair.create method:

import { KeyPair } from 'ucan-storage/keypair'

// KeyPair.create returns a promise, so it should be called from an async function or resolved with `.then`
async function createNewKeypair() {
  const kp = await KeyPair.create()

  // log the DID string for the public key to the console:
  console.log(kp.did())
}

To generate a keypair using the CLI, use the ucan-storage keypair command:

npx ucan-storage keypair

You should see output similar to this:

DID:           did:key:z6MkvxqUDNrq2QJhsMozHJnxPDHmv6KYpU9FmKKzWvxbUewg
Public Key:    9U6gpYLouaQ2az2YlIWqBUQX1KWNiyDFmpVNZMcLBw8=
Private Key:   sCpzGR3vC7qHGxhT7Wg6yvbhvHQABigLH 0 egJrV6o=

Important: when saving to disk, take care to save your private key in a secure location!

Saving and loading keypairs

You can export your private key to a string that can be saved to disk with the export method and load an exported key with the static fromExportedKey method:

import fs from 'fs'
import { KeyPair } from 'ucan-storage/keypair'

async function createAndSaveKeypair(outputFilename) {
  const kp = await KeyPair.create()
  await fs.promises.writeFile(kp.export())
  return kp
}

async function loadKeyPairFromFile(keypairFilename) {
  const exportedKey = await fs.promises.readFile(keypairFilename)
  return KeyPair.fromExportedKey(exportedKey)
}

Important: when saving to disk, take care to save your keys in a secure location!

You can get the public key and DID for a private key using the ucan-storage keypair --from <private-key> CLI command:

npx ucan-storage keypair --from sCpzGR3vC7qHGxhT7Wg6yvbhvHQABigLH 0 egJrV6o=

Output:

DID:           did:key:z6MkvxqUDNrq2QJhsMozHJnxPDHmv6KYpU9FmKKzWvxbUewg
Public Key:    9U6gpYLouaQ2az2YlIWqBUQX1KWNiyDFmpVNZMcLBw8=
Private Key:   sCpzGR3vC7qHGxhT7Wg6yvbhvHQABigLH 0 egJrV6o=

Creating a UCAN token

The build function is used to create new UCAN tokens from a UcanStorageOptions input object.

The issuer option must be set to a KeyPair object. The private key will be used to sign the token, and the public key will be used to set the iss field in the token payload.

The audience option must contain the DID string for the recipient's public key.

The capabilities option must contain one or more StorageCapability objects that represent the capabilities the token enables. If you are creating a token that derives capabilities from a "parent" UCAN token, the capabilities you pass in must be a subset of the capabilities granted by the parent UCAN. See the section on Storage capabilities to learn more.

When creating a "child" UCAN based on another "parent" UCAN, the parent token (in its JWT string form) should be included in the proofs array in the UcanStorageOptions object.

You can restrict the lifetime of the token by either setting an explicit expiration timestamp or setting a lifetimeInSeconds option. If both are set, expiration takes precedence.

You can also issue tokens that will become valid at a future date by setting the notBefore option to a timestamp in the future. If notBefore and expiration are both set, notBefore must be less than expiration.

Both timestamp options (expiration and notBefore) are Unix timestamps (seconds elapsed since the Unix epoch).

Creating a root token

You can create a "root token" with no parent by omitting the proofs field when calling the build function. This is generally only used in production by storage service providers (e.g. NFT.Storage) to issue tokens to users and marketplaces, but it is useful for all participants when writing tests, etc.

import { build } from 'ucan-storage/ucan-storage'

async function makeRootToken(
  issuerKeyPair,
  audienceDID,
  actions = ['upload/*']
) {
  // make "capability" objects from the actions
  const capabilities = actions.map((action) => ({
    with: `storage://${audienceDID}`,
    can: action,
  }))

  const token = await build({
    issuer: issuerKeyPair,
    audience: audienceDID,
    capabilities,
  })
}

You can create a root token CLI with the ucan-storage ucan command.

npx ucan-storage ucan \
  --audience did:key:z6MkvxqUDNrq2QJhsMozHJnxPDHmv6KYpU9FmKKzWvxbUewg \
  --with storage://did:key:z6MkvxqUDNrq2QJhsMozHJnxPDHmv6KYpU9FmKKzWvxbUewg \
  --can 'upload/*'

Note that the example above omits the --issuer flag, so a new keypair will be randomly generated and used to sign the UCAN token. This is not very useful, so you should run the command with --issuer <your-private-key>.

It's also best to set an expiration time, so that the UCAN does not remain valid indefinitely. The --expiration flag accepts a timestamp in ISO 8601 format.

Deriving a child token

If you have a UCAN token, you can create a "child token" that derives capabilities from the parent token. To do so, include the parent token in the proofs array when calling build, and make sure that the capabilities you include do not exceed the capabilities in the parent token.

In this example, we first validate the parent token, which returns the parsed UCAN payload. From the payload, we can retrieve the capabilities from the att field and extend the resource path to include the DID of the "audience" for the new token.

import { build, validate } from 'ucan-storage/ucan-storage'

async function deriveToken(parentUCAN, issuerKeyPair, audienceDID) {
  // validate the parent UCAN and extract the payload
  const { payload } = await validate(parentUCAN)

  // the `att` field contains the capabilities
  const { att } = payload

  // for each capability in the parent, keep everything except the
  // resource path, to which we append the DID for the new token's audience
  const capabilities = att.map((capability) => ({
    ...capability,
    with: [capability.with, audienceDID].join('/'),
  }))

  // include the parent UCAN JWT string in the proofs array
  const proofs = [parentUCAN]

  const token = await build({
    issuer: issuerKeyPair,
    audience: audienceDID,
    capabilities,
    proofs,
  })
}

You can also use the CLI to derive child UCAN tokens by adding the --proof flag when calling the ucan-storage ucan command:

npx ucan-storage ucan \
  --audience did:key:z6MkvxqUDNrq2QJhsMozHJnxPDHmv6KYpU9FmKKzWvxbUewg \
  --with storage://did:key:z6MkvxqUDNrq2QJhsMozHJnxPDHmv6KYpU9FmKKzWvxbUewg \
  --can 'upload/*' \
  --proof eyJhb...etc...

Pass in complete parent UCAN token as the argument to the --proof flag to include it in the proof chain of the generated token.

Creating a request token to upload content

When uploading content to the storage service, the user will need to generate a UCAN token using their keypair and attach this "request token" to the upload request in a header.

This token must have the DID for the storage service as the audience, with the end-user's DID as the issuer.

The chain of proofs must include a UCAN token issued by the storage service, and the token must include capabilities sufficient to serve the request. This "proof token" may be issued by the storage service itself, or by a third party like an NFT marketplace who has derived a child token to delegate storage services to end users.

The capabilites field for the request token should include the capabilites from UCAN token that was issued to the user. These are found in the att field of the UCAN token payload and can be copied into the request token unmodified.

import { build } from 'ucan-storage/ucan-storage'

// The DID for the storage service. In real code, you should obtain this from the service you're targetting.
const serviceDID = 'did:key:a-fake-service-did'

async function createRequestToken(parentUCAN, issuerKeyPair) {
  // we want to include the capabilities of the parent token in our request token
  // so we validate the parent token to extract the payload and copy over the capabilities
  const { payload } = await validate(parentUCAN)

  // the `att` field contains the capabilities we need for uploading
  const { att } = payload

  return build({
    issuer: issuerKeyPair,
    audience: serviceDID,
    capabilities: att,
    proofs: [parentUcan],
  })
}

Setting an expiration date

When creating a UCAN, you can set it to expire at a certain date by setting the lifetimeInSeconds or expiration options when calling build.

The expiration option sets a point in time at which the token will expire. All parties in the UCAN flow should reject tokens with expiration dates in the past.

You can create tokens that will not be valid unitl a time in the future by setting the notBefore option.

Both expiration and notBefore are Unix timestamps, which record the number of seconds elapsed since the start of the Unix "epoch". JavaScript's Date.now() method returns epoch timestamps with millisecond resolution, so to get the correct value you must divide by 1000:

// convert timestamp to seconds
const nowInSeconds = Math.floor(Date.now() / 1000)

// expire in one minute
const expiration = nowInSeconds   60

The lifetimeInSeconds option is a helper to set the expiration date relative to the current time, or the notBefore time if specified.

When creating UCAN tokens using the CLI, pass in an ISO 8601-formatted timestamp to the --expiration flag.

Here's an example of generating an ISO 8601 date for 10 minutes past the current time on a few platforms:

macOS / BSD coreutils:

date  %Y-%m-%dT%H:%M:%S%z -d $(date)   10 minutes

GNU coreutils (found on most Linux distributions):

date --iso-8601=seconds -d $(date)   10 minutes

Windows PowerShell:

(Get-Date).AddMinutes(10).Format("o")

Validating a token

You can validate a UCAN token using the validate function, which accepts a JWT string and returns the parsed token contents if the token is valid.

If validation fails, the Promise returned by validate will reject with an Error, so it's important to surround calls to validate with try/catch statements when calling from async functions, or use .catch() to handle errors if resolving Promises manually.

import { validate } from 'ucan-storage/ucan-storage'

async function validateUCAN(ucanJWTString) {
  try {
    const { header, payload, signature } = await validate(ucanJWTString)
    console.log('UCAN is valid!')
    console.log('header:', header)
    console.log('payload:' payload)
  } catch (err) {
    console.error('UCAN validation failed: ', err)
  }
}

You can make the validation more lenient by passing in a ValidateOptions object and disabling the validation checks you want to skip. For example, if you want to ignore the notBefore timestamp, you can set the checkIsTooEarly option to false:

import { validate } from 'ucan-storage'

async function validateIgnoringEarlyBirds(ucanJWTString) {
  try {
    const validateOptions = {
      checkIsTooEarly: false,
    }
    const { header, payload, signature } = await validate(
      ucanJWTString,
      validateOptions
    )
    console.log('UCAN is valid! Early? No problem!')
  } catch (err) {
    console.error('UCAN validation failed: ', err)
  }
}

You can validate a token using the CLI with the ucan-storage validate command:

npx ucan-storage validate eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsInVjdiI6IjAuOC4wIn0.eyJhdWQiOiJkaWQ6a2V5Ono2TWt2eHFVRE5ycTJRSmhzTW96SEpueFBESG12NktZcFU5Rm1LS3pXdnhiVWV3ZyIsImF0dCI6W3sid2l0aCI6InN0b3JhZ2U6Ly9kaWQ6a2V5Ono2TWt2eHFVRE5ycTJRSmhzTW96SEpueFBESG12NktZcFU5Rm1LS3pXdnhiVWV3ZyIsImNhbiI6InVwbG9hZC8qIn1dLCJleHAiOjE2NDY3NjkwNTAsImlzcyI6ImRpZDprZXk6ejZNa3NtRGRqVlAxWUhqd1pNd3FXaVFjWDFEWE00cGphNml5a2h6Q3hOZTlTaW5YIiwicHJmIjpbbnVsbF19.rOHzMzBWaFbH4tqS7aJ_4rBPkZbYQkck-fZLPD0skK3iRZUnxNUEFQITav5v70jzAwJIj757Xk2ImwOwmZ-4Dg

Example output:

Issuer: did:key:z6MksmDdjVP1YHjwZMwqWiQcX1DXM4pja6iykhzCxNe9SinX
Audience: did:key:z6MkvxqUDNrq2QJhsMozHJnxPDHmv6KYpU9FmKKzWvxbUewg
Expires: 2022-03-08T19:50:50.000Z
Capabilities: [
  {
    "with": "storage://did:key:z6MkvxqUDNrq2QJhsMozHJnxPDHmv6KYpU9FmKKzWvxbUewg",
    "can": "upload/*"
  }
]
Proofs: [
  null
]

Using UCANs with NFT.Storage

Use of UCANs to delegate upload permissions in NFT.Storage is currently a Preview Feature. If you use and have any feedback, please leave feedback in this Github Discussion!

NFT.Storage is a free service for storing NFT data on the decentralized Filecoin storage network, with content retrieval via IPFS.

NFT.Storage is the first service to support UCAN-based authorization using the ucan-storage library.

For marketplaces and other platforms, adopting UCAN auth can allow you to integrate free, decentralized NFT storage into your own applications without requiring your end users to sign up for an NFT.Storage account.

The NFT.Storage API includes endpoints for registering your DID with your NFT.Storage account and obtaining "root tokens" that can be used to delegate storage permissions to other users, whether they have an NFT.Storage account or not.

If you have not yet created an NFT.Storage account, see the NFT.Storage documentation.

To use the UCAN API endpoints, create an API token at your NFT.Storage account management page.

Registering your DID

Once you have a normal API token, you can generate a keypair using the ucan-storage CLI and call an API endpoint to register the DID of the public key with the NFT.Storage service.

To register your DID, send a POST request to https://api.nft.storage/user/did with a body containing a JSON object of the form:

{
  "did": "<your-did-string>"
}

In the example below, replace $API_TOKEN with your NFT.Storage API token, or set a shell variable named API_TOKEN before running the command.

Likewise, replace $DID with your DID string, or set a shell variable named DID before running the command.

curl -X POST -H "Authorization: Bearer $API_TOKEN" -H 'Content-Type: application/json' --data "{\"did\": \"$DID\"}"

Obtaining a root UCAN token

Once you've registered your DID, you can request a root UCAN token from the NFT.Storage API, which will be valid for a duration of two weeks.

To request a root token, you must have either a normal API token or an existing root UCAN token. By providing an existing UCAN, you can "refresh" a token before it expires.

Send a POST request to https://api.nft.storage/ucan/token to obtain a new UCAN token.

In the example below, replace $TOKEN with either an existing UCAN token or an NFT.Storage API token. Or, set a shell variable named TOKEN before running the command.

curl -X POST -H "Authorization: Bearer $TOKEN" https://api.nft.storage/ucan/token

You can use the root token to derive child UCAN tokens for other users, or to create a request token to upload content using UCAN auth instead of your API token.

Obtaining the service DID

The DID for the NFT.Storage service is available at the public endpoint https://api.nft.storage/did.

Send a GET request to https://api.nft.storage/did, which should return a JSON object of the form:

{
  "ok": true,
  "value": "<service-did>"
}

The value field contains the service DID, which is used when creating request tokens.

Contributing

We use pnpm in this project and commit the pnpm-lock.yaml file.

Install dependencies.

# install all dependencies in the mono-repo
pnpm install
# setup git hooks
npx simple-git-hooks