Skip to content

Commit

Permalink
Removes ability to construct token header (#51)
Browse files Browse the repository at this point in the history
* Removes ability to construct token header

* Fix typo

* enclose → sign

* Feedback error messages
  • Loading branch information
icidasset authored Feb 11, 2022
1 parent 118670b commit 92d2281
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 104 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# ts-ucan
# ts-ucan
[![NPM](https://img.shields.io/npm/v/ucans)](https://www.npmjs.com/package/ucans)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/fission-suite/blob/master/LICENSE)
[![Discussions](https://img.shields.io/github/discussions/ucan-wg/ts-ucan)](https://github.com/ucan-wg/ts-ucan/discussions)
Expand All @@ -10,7 +10,7 @@ At a high level, UCANs (“User Controlled Authorization Network”) are an auth
No all-powerful authorization server or server of any kind is required for UCANs. Instead, everything a user can do is captured directly in a key or token, which can be sent to anyone who knows how to interpret the UCAN format. Because UCANs are self-contained, they are easy to consume permissionlessly, and they work well offline and in distributed systems.


UCANs work
UCANs work
- Server -> Server
- Client -> Server
- Peer-to-peer
Expand Down Expand Up @@ -120,8 +120,8 @@ const u = await ucan.build({
const token = ucan.encode(u) // base64 jwt-formatted auth token

// You can also use your own signing function if you're bringing your own key management solution
const { header, payload } = await ucan.buildParts(...)
const u = await ucan.addSignature(header, payload, signingFn)
const payload = await ucan.buildPayload(...)
const u = await ucan.sign(payload, keyType, signingFn)
```

## Sponsors
Expand Down
39 changes: 19 additions & 20 deletions src/builder.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as token from "./token"
import * as util from "./util"
import { Keypair, isKeypair, Capability, isCapability, Fact, UcanParts } from "./types"
import { publicKeyBytesToDid } from "./did/transformers"
import { Capability, Keypair, Fact, UcanPayload, isKeypair, isCapability } from "./types"
import { CapabilityInfo, CapabilitySemantics, canDelegate } from "./attenuation"
import { Chained } from "./chained"
import { canDelegate, CapabilityInfo, CapabilitySemantics } from "./attenuation"
import { Store } from "./store"
import { publicKeyBytesToDid } from "./did/transformers"


export interface BuildableState {
Expand Down Expand Up @@ -42,11 +42,11 @@ function isCapabilityLookupCapableState(obj: unknown): obj is CapabilityLookupCa

/**
* A builder API for UCANs.
*
*
* Supports grabbing UCANs from a UCAN `Store` for proofs (see `delegateCapability`).
*
*
* Example usage:
*
*
* ```ts
* const ucan = await Builder.create()
* .issuedBy(aliceKeypair)
Expand All @@ -73,15 +73,15 @@ export class Builder<State extends Partial<BuildableState>> {
* - `issuedBy`
* - `toAudience` and
* - `withLifetimeInSeconds` or `withExpiration`.
* To finalise the builder, call its `build` or `buildParts` method.
* To finalise the builder, call its `build` or `buildPayload` method.
*/
static create(): Builder<Record<string, never>> {
return new Builder({}, { capabilities: [], facts: [], proofs: [], addNonce: false })
}

/**
* @param issuer The issuer as a DID string ("did:key:...").
*
*
* The UCAN must be signed with the private key of the issuer to be valid.
*/
issuedBy(issuer: Keypair): Builder<State & { issuer: Keypair }> {
Expand All @@ -93,7 +93,7 @@ export class Builder<State extends Partial<BuildableState>> {

/**
* @param audience The audience as a DID string ("did:key:...").
*
*
* This is the identity this UCAN transfers rights to.
* It could e.g. be the DID of a service you're posting this UCAN as a JWT to,
* or it could be the DID of something that'll use this UCAN as a proof to
Expand Down Expand Up @@ -186,14 +186,14 @@ export class Builder<State extends Partial<BuildableState>> {

/**
* Delegate capabilities from a given proof to the audience of the UCAN you're building.
*
*
* @param semantics The semantics for how delgation works for given capability.
* @param requiredCapability The capability you want to delegate.
*
*
* Then, one of
* @param proof The proof chain that grants the issuer of this UCAN at least the capabilities you want to delegate, or
* @param store The UCAN store in which to try to find a UCAN granting you enough capabilities to delegate given capabilities.
*
*
* @throws If given store can't provide a UCAN for delegating given capability
* @throws If given proof can't be used to delegate given capability
* @throws If the builder hasn't set an issuer and expiration yet
Expand Down Expand Up @@ -255,15 +255,14 @@ export class Builder<State extends Partial<BuildableState>> {
}

/**
* Build the UCAN header and body. This can be used if you want to sign the UCAN yourself afterwards.
* Build the UCAN body. This can be used if you want to sign the UCAN yourself afterwards.
*/
buildParts(): State extends BuildableState ? UcanParts : never
buildParts(): UcanParts {
buildPayload(): State extends BuildableState ? UcanPayload : never
buildPayload(): UcanPayload {
if (!isBuildableState(this.state)) {
throw new Error(`Builder is missing one of the required properties before it can be built: issuer, audience and expiration.`)
}
return token.buildParts({
keyType: this.state.issuer.keyType,
return token.buildPayload({
issuer: publicKeyBytesToDid(this.state.issuer.publicKey, this.state.issuer.keyType),
audience: this.state.audience,

Expand All @@ -279,16 +278,16 @@ export class Builder<State extends Partial<BuildableState>> {

/**
* Finalize: Build and sign the UCAN.
*
*
* @throws If the builder hasn't yet been set an issuer, audience and expiration.
*/
async build(): Promise<State extends BuildableState ? Chained : never>
async build(): Promise<Chained> {
if (!isBuildableState(this.state)) {
throw new Error(`Builder is missing one of the required properties before it can be built: issuer, audience and expiration.`)
}
const parts = this.buildParts()
const signed = await token.sign(parts.header, parts.payload, this.state.issuer)
const payload = this.buildPayload()
const signed = await token.signWithKeypair(payload, this.state.issuer)
const encoded = token.encode(signed)
return new Chained(encoded, { ...signed, payload: { ...signed.payload, prf: this.defaultable.proofs } })
}
Expand Down
105 changes: 51 additions & 54 deletions src/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ import * as util from "./util"
import { handleCompatibility } from "./compatibility"
import { verifySignatureUtf8 } from "./did/validation"
import { Capability, Fact, Keypair, KeyType } from "./types"
import { Ucan, UcanHeader, UcanPayload, UcanParts } from "./types"
import { Ucan, UcanHeader, UcanPayload } from "./types"


// CONSTANTS


const TYPE = "JWT"
const VERSION = "0.7.0"



// COMPOSING
Expand Down Expand Up @@ -48,26 +56,18 @@ export async function build(params: {
facts?: Array<Fact>
proofs?: Array<string>
addNonce?: boolean

// in the weeds
ucanVersion?: string
}): Promise<Ucan> {
const keypair = params.issuer
const didStr = did.publicKeyBytesToDid(keypair.publicKey, keypair.keyType)
const { header, payload } = buildParts({
...params,
issuer: didStr,
keyType: keypair.keyType
})
return sign(header, payload, keypair)
const payload = buildPayload({ ...params, issuer: didStr })
return signWithKeypair(payload, keypair)
}

/**
* Create an unsigned UCAN, User Controlled Authorization Networks, JWT.
* Construct the payload for a UCAN.
*/
export function buildParts(params: {
export function buildPayload(params: {
// from/to
keyType: KeyType
issuer: string
audience: string

Expand All @@ -83,12 +83,8 @@ export function buildParts(params: {
facts?: Array<Fact>
proofs?: Array<string>
addNonce?: boolean

// in the weeds
ucanVersion?: string
}): UcanParts {
}): UcanPayload {
const {
keyType,
issuer,
audience,
capabilities = [],
Expand All @@ -97,75 +93,75 @@ export function buildParts(params: {
notBefore,
facts,
proofs = [],
addNonce = false,
ucanVersion = "0.7.0"
addNonce = false
} = params

// Timestamps
const currentTimeInSeconds = Math.floor(Date.now() / 1000)
const exp = expiration || (currentTimeInSeconds + lifetimeInSeconds)

const header = {
alg: jwtAlgorithm(keyType),
typ: "JWT",
ucv: ucanVersion,
} as UcanHeader

const payload = {
// 📦
return {
aud: audience,
att: capabilities,
exp,
fct: facts,
iss: issuer,
nbf: notBefore,
nnc: addNonce ? util.generateNonce() : undefined,
prf: proofs,
} as UcanPayload

if (addNonce) {
payload.nnc = util.generateNonce()
}

// Issuer key type must match UCAN algorithm
if (did.didToPublicKey(payload.iss).type !== keyType) {
throw new Error("The issuer's key type must match the given key type.")
}

// 📦
return { header, payload }
}

/**
* Add a signature to a UCAN based on the given key pair.
* Encloses a UCAN payload as to form a finalised UCAN.
*/
export async function sign(
header: UcanHeader,
payload: UcanPayload,
keypair: Keypair
): Promise<Ucan> {
return addSignature(header, payload, (data) => keypair.sign(data))
}

/**
* Add a signature to a UCAN based based on the given signature function.
*/
export async function addSignature(
header: UcanHeader,
payload: UcanPayload,
keyType: KeyType,
signFn: (data: Uint8Array) => Promise<Uint8Array>
): Promise<Ucan> {
const header: UcanHeader = {
alg: jwtAlgorithm(keyType),
typ: TYPE,
ucv: VERSION,
}

// Issuer key type must match UCAN algorithm
if (did.didToPublicKey(payload.iss).type !== keyType) {
throw new Error("The issuer's key type must match the given key type.")
}

// Encode parts
const encodedHeader = encodeHeader(header)
const encodedPayload = encodePayload(payload)

// Sign
const toSign = uint8arrays.fromString(`${encodedHeader}.${encodedPayload}`, "utf8")
const sig = await signFn(toSign)

// 📦
return {
header,
payload,
signature: uint8arrays.toString(sig, "base64url")
}
}

/**
* `sign` with a `Keypair`.
*/
export async function signWithKeypair(
payload: UcanPayload,
keypair: Keypair
): Promise<Ucan> {
return sign(
payload,
keypair.keyType,
data => keypair.sign(data)
)
}



// ENCODING
Expand Down Expand Up @@ -344,10 +340,11 @@ export const isTooEarly = (ucan: Ucan): boolean => {
/**
* JWT algorithm to be used in a JWT header.
*/
function jwtAlgorithm(keyType: KeyType): string | null {
function jwtAlgorithm(keyType: KeyType): string {
switch (keyType) {
case "bls12-381": throw new Error(`Unknown KeyType "${keyType}"`)
case "ed25519": return "EdDSA"
case "rsa": return "RS256"
default: return null
default: throw new Error(`Unknown KeyType "${keyType}"`)
}
}
Loading

0 comments on commit 92d2281

Please sign in to comment.