Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: update to ucan spec v0.10 #107

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 66 additions & 9 deletions packages/core/src/compatibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import * as semver from "./semver.js"

import * as util from "./util.js"
import { SUPERUSER } from "./capability/super-user.js"
import { UcanParts, isUcanHeader, isUcanPayload } from "./types.js"
import { my } from "./capability/index.js"
import { isUcanHeader, isUcanPayload, Fact } from "./types.js"
import { Capability, EncodedCapability, isCapability, isEncodedCapability, my } from "./capability/index.js"


const VERSION_0_3 = { major: 0, minor: 3, patch: 0 }
Expand Down Expand Up @@ -44,13 +44,61 @@ function isUcanPayload_0_3_0(obj: unknown): obj is UcanPayload_0_3_0 {
&& util.hasProp(obj, "ptc") && typeof obj.ptc === "string"
&& (!util.hasProp(obj, "prf") || typeof obj.prf === "string")
}
type UcanHeader_0_8_0 = {
alg: string
typ: string
uav: string
}

type UcanPayload_0_8_0 = {
iss: string
aud: string
exp: number
nbf?: number
nnc?: string
att: Array<EncodedCapability | Capability>
fct?: Array<Fact>
prf: Array<string>
}

export function handleCompatibility(header: unknown, payload: unknown): UcanParts {
function isUcanHeader_0_8_0(obj: unknown): obj is UcanHeader_0_8_0 {
return isUcanHeader_0_3_0(obj)
}

function isUcanPayload_0_8_0(obj: unknown): obj is UcanPayload_0_8_0 {
return util.isRecord(obj)
&& util.hasProp(obj, "iss") && typeof obj.iss === "string"
&& util.hasProp(obj, "aud") && typeof obj.aud === "string"
&& util.hasProp(obj, "exp") && typeof obj.exp === "number"
&& (!util.hasProp(obj, "nbf") || typeof obj.nbf === "number")
&& (!util.hasProp(obj, "nnc") || typeof obj.nnc === "string")
&& util.hasProp(obj, "att") && Array.isArray(obj.att) && obj.att.every(a => isCapability(a) || isEncodedCapability(a))
&& (!util.hasProp(obj, "fct") || Array.isArray(obj.fct) && obj.fct.every(util.isRecord))
&& util.hasProp(obj, "prf") && Array.isArray(obj.prf) && obj.prf.every(str => typeof str === "string")
}


export function handleCompatibility(header: unknown, payload: unknown) {
const fail = (place: string, reason: string) => new Error(`Can't parse UCAN ${place}: ${reason}`)

if (!util.isRecord(header)) throw fail("header", "Invalid format: Expected a record")

if (util.hasProp(payload, "ucv")) {
// Version >= 0.10
if (typeof payload.ucv !== "string") {
throw fail("payload", "Invalid format: field 'ucv' is not a string")
}
payload.ucv = semver.parse(payload.ucv)
if (payload.ucv == null) {
throw fail("payload", "Invalid format: 'ucv' string cannot be parsed into a semantic version")
}
if (!isUcanHeader(header)) throw fail("header", "Invalid format")
if (!isUcanPayload(payload)) throw fail("payload", "Invalid format")
return { header, payload }
}

// Handle < 0.10 (actually, only <= 0.8 are supported, afaik) //

// parse either the "ucv" or "uav" as a version in the header
// we translate 'uav: 1.0.0' into 'ucv: 0.3.0'
let version: "0.9.1" | "0.3.0" = "0.9.1"
Expand All @@ -69,13 +117,22 @@ export function handleCompatibility(header: unknown, payload: unknown): UcanPart
if (typeof header.ucv !== "string") {
throw fail("header", "Invalid format: Missing 'ucv' key or 'ucv' is not a string")
}
header.ucv = semver.parse(header.ucv)
if (header.ucv == null) {
const ucv = semver.parse(header.ucv)
if (ucv == null) {
throw fail("header", "Invalid format: 'ucv' string cannot be parsed into a semantic version")
}
if (!isUcanHeader(header)) throw fail("header", "Invalid format")
if (!isUcanPayload(payload)) throw fail("payload", "Invalid format")
return { header, payload }
if (!isUcanHeader_0_8_0(header)) throw fail("header", "Invalid format")
if (!isUcanPayload_0_8_0(payload)) throw fail("payload", "Invalid format")
return {
header: {
alg: header.alg,
typ: header.typ,
},
payload: {
...payload,
ucv: ucv,
}
}
}

// we know it's version 0.3.0
Expand All @@ -86,9 +143,9 @@ export function handleCompatibility(header: unknown, payload: unknown): UcanPart
header: {
alg: header.alg,
typ: header.typ,
ucv: VERSION_0_3,
},
payload: {
ucv: VERSION_0_3,
iss: payload.iss,
aud: payload.aud,
nbf: payload.nbf,
Expand Down
23 changes: 10 additions & 13 deletions packages/core/src/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { handleCompatibility } from "./compatibility.js"


const TYPE = "JWT"
const VERSION = { major: 0, minor: 8, patch: 1 }
const VERSION = { major: 0, minor: 10, patch: 0 }



Expand All @@ -29,10 +29,10 @@ const VERSION = { major: 0, minor: 8, patch: 1 }
*
* `alg`, Algorithm, the type of signature.
* `typ`, Type, the type of this data structure, JWT.
* `ucv`, UCAN version.
*
* ### Payload
*
* `ucv`, UCAN Semantic Version.
* `att`, Attenuation, a list of resources and capabilities that the ucan grants.
* `aud`, Audience, the ID of who it's intended for.
* `exp`, Expiry, unix timestamp of when the jwt is no longer valid.
Expand Down Expand Up @@ -110,6 +110,7 @@ export function buildPayload(params: {

// 📦
return {
ucv: VERSION,
aud: audience,
att: capabilities,
exp,
Expand All @@ -132,7 +133,6 @@ export const sign = (plugins: Plugins) =>
const header: UcanHeader = {
alg: jwtAlg,
typ: TYPE,
ucv: VERSION,
}

// Issuer key type must match UCAN algorithm
Expand Down Expand Up @@ -195,12 +195,8 @@ export function encode(ucan: Ucan<unknown>): string {
* @returns The header of a UCAN encoded as url-safe base64 JSON
*/
export function encodeHeader(header: UcanHeader): string {
const headerFormatted = {
...header,
ucv: semver.format(header.ucv)
}
return uint8arrays.toString(
uint8arrays.fromString(JSON.stringify(headerFormatted), "utf8"),
uint8arrays.fromString(JSON.stringify(header), "utf8"),
"base64url"
)
}
Expand All @@ -217,6 +213,7 @@ export function encodeHeader(header: UcanHeader): string {
export function encodePayload(payload: UcanPayload): string {
const payloadWithEncodedCaps = {
...payload,
ucv: semver.format(payload.ucv),
att: payload.att.map(capability.encode)
}

Expand Down Expand Up @@ -282,8 +279,8 @@ export function parse(encodedUcan: string): UcanParts {
// Ensure proper types/structure
const parsedAttenuations = payload.att.reduce((acc: Capability[], cap: unknown): Capability[] => {
return isEncodedCapability(cap)
? [ ...acc, capability.parse(cap) ]
: isCapability(cap) ? [ ...acc, cap ] : acc
? [...acc, capability.parse(cap)]
: isCapability(cap) ? [...acc, cap] : acc // ?! silently dropping if not valid
}, [])

// Fin
Expand Down Expand Up @@ -407,8 +404,8 @@ export const validateProofs = (plugins: Plugins) =>
throw new Error(`Invalid Proof: Expiration (${proof.payload.exp}) is before parent's 'not before' (${ucan.payload.nbf})`)
}

if (checkVersionMonotonic && semver.lt(ucan.header.ucv, proof.header.ucv)) {
throw new Error(`Invalid Proof: Version (${proof.header.ucv}) is higher than parent's version (${ucan.header.ucv})`)
if (checkVersionMonotonic && semver.lt(ucan.payload.ucv, proof.payload.ucv)) {
throw new Error(`Invalid Proof: Version (${proof.payload.ucv}) is higher than parent's version (${ucan.payload.ucv})`)
}

yield proof
Expand Down Expand Up @@ -439,4 +436,4 @@ export function isExpired(ucan: Ucan): boolean {
export const isTooEarly = (ucan: Ucan): boolean => {
if (ucan.payload.nbf == null) return false
return ucan.payload.nbf > Math.floor(Date.now() / 1000)
}
}
6 changes: 3 additions & 3 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ export interface UcanParts<Prf = string> {
export type UcanHeader = {
alg: string
typ: string
ucv: SemVer
}

export type UcanPayload<Prf = string> = {
ucv: SemVer
iss: string
aud: string
exp: number
Expand Down Expand Up @@ -156,11 +156,11 @@ export function isUcanHeader(obj: unknown): obj is UcanHeader {
return util.isRecord(obj)
&& util.hasProp(obj, "alg") && typeof obj.alg === "string"
&& util.hasProp(obj, "typ") && typeof obj.typ === "string"
&& util.hasProp(obj, "ucv") && semver.isSemVer(obj.ucv)
}

export function isUcanPayload(obj: unknown): obj is UcanPayload {
return util.isRecord(obj)
&& util.hasProp(obj, "ucv") && semver.isSemVer(obj.ucv)
&& util.hasProp(obj, "iss") && typeof obj.iss === "string"
&& util.hasProp(obj, "aud") && typeof obj.aud === "string"
&& util.hasProp(obj, "exp") && typeof obj.exp === "number"
Expand All @@ -169,4 +169,4 @@ export function isUcanPayload(obj: unknown): obj is UcanPayload {
&& util.hasProp(obj, "att") && Array.isArray(obj.att) && obj.att.every(a => isCapability(a) || isEncodedCapability(a))
&& (!util.hasProp(obj, "fct") || Array.isArray(obj.fct) && obj.fct.every(util.isRecord))
&& util.hasProp(obj, "prf") && Array.isArray(obj.prf) && obj.prf.every(str => typeof str === "string")
}
}
Loading