diff --git a/.github/workflows/principal.yml b/.github/workflows/principal.yml
index f22b87b3..26051f96 100644
--- a/.github/workflows/principal.yml
+++ b/.github/workflows/principal.yml
@@ -51,7 +51,6 @@ jobs:
strategy:
matrix:
node-version:
- - 14
- 16
os:
- ubuntu-latest
diff --git a/packages/interface/src/lib.ts b/packages/interface/src/lib.ts
index 0fd7aea4..bd5cabd2 100644
--- a/packages/interface/src/lib.ts
+++ b/packages/interface/src/lib.ts
@@ -15,7 +15,7 @@ import {
Signature,
Principal,
Verifier,
- Signer,
+ Signer as UCANSigner,
} from '@ipld/dag-ucan'
import * as UCAN from '@ipld/dag-ucan'
import {
@@ -39,8 +39,6 @@ export * from './transport.js'
export type {
Transport,
Principal,
- Verifier,
- Signer,
Phantom,
Tuple,
DID,
@@ -388,9 +386,61 @@ export type URI
= `${P}${string}` &
}>
export interface PrincipalParser {
- parse(did: UCAN.DID): UCAN.Verifier
+ parse(did: UCAN.DID): Verifier
+}
+
+/**
+ * Represents component that can create a signer from it's archive. Usually
+ * signer module would provide `from` function and therefor be implementation
+ * of this interface.
+ * Library also provides utility functions for combining multiple
+ * SignerImporters into one.
+ */
+export interface SignerImporter {
+ from(archive: SignerArchive): Self
+}
+
+export interface Signer
+ extends UCANSigner {
+ /**
+ * Returns archive of this signer which is byte encoded form when signer key
+ * is extractable and is {@link SignerInfo} form otherwise. This allows a user
+ * to store non extractable archives in indexedDB and store extractable
+ * archives on disk, which matches general expectation that in browsers
+ * unextratable keys should be used and extractable keys in node.
+ *
+ * @example
+ * ```ts
+ * const save = async (signer: Signer) => {
+ * const archive = signer.toArchive()
+ * if (archive instanceof Uint8Array) {
+ * await fs.writeFile(KEY_PATH, archive)
+ * } else {
+ * await IDB_OBJECT_STORE.add(archive)
+ * }
+ * }
+ * ```
+ */
+ toArchive(): SignerArchive>
+}
+
+export interface SignerInfo {
+ readonly did: ReturnType
+ readonly key: CryptoKey
}
+export type SignerArchive =
+ | ByteView
+ | SignerInfo
+
+export { Verifier }
+
export type InferInvokedCapability<
C extends CapabilityParser>
> = C extends CapabilityParser> ? T : never
+
+export type Intersection = (T extends any ? (i: T) => void : never) extends (
+ i: infer I
+) => void
+ ? I
+ : never
diff --git a/packages/principal/package.json b/packages/principal/package.json
index 4ce0c7a4..71d5da96 100644
--- a/packages/principal/package.json
+++ b/packages/principal/package.json
@@ -30,7 +30,8 @@
"@ipld/dag-ucan": "^4.0.0-beta",
"@noble/ed25519": "^1.7.0",
"@ucanto/interface": "^1.0.0",
- "multiformats": "^9.8.1"
+ "multiformats": "^9.8.1",
+ "one-webcrypto": "^1.0.3"
},
"devDependencies": {
"@types/chai": "^4.3.3",
@@ -52,6 +53,9 @@
],
"ed25519": [
"dist/src/ed25519.d.ts"
+ ],
+ "rsa": [
+ "dist/src/rsa.d.ts"
]
}
},
@@ -59,6 +63,10 @@
"./ed25519": {
"types": "./dist/src/ed25519.d.ts",
"import": "./src/ed25519.js"
+ },
+ "./rsa": {
+ "types": "./dist/src/rsa.d.ts",
+ "import": "./src/rsa.js"
}
},
"c8": {
diff --git a/packages/principal/src/ed25519/signer.js b/packages/principal/src/ed25519/signer.js
index 83cbabd3..f5311fb2 100644
--- a/packages/principal/src/ed25519/signer.js
+++ b/packages/principal/src/ed25519/signer.js
@@ -1,12 +1,14 @@
import * as ED25519 from '@noble/ed25519'
import { varint } from 'multiformats'
-import * as API from '@ucanto/interface'
+import * as API from './type.js'
import * as Verifier from './verifier.js'
-import { base64pad, base64url } from 'multiformats/bases/base64'
+import { base64pad } from 'multiformats/bases/base64'
import * as Signature from '@ipld/dag-ucan/signature'
export const code = 0x1300
export const name = Verifier.name
+export const signatureAlgorithm = Verifier.signatureAlgorithm
+export const signatureCode = Verifier.signatureCode
const PRIVATE_TAG_SIZE = varint.encodingLength(code)
const PUBLIC_TAG_SIZE = varint.encodingLength(Verifier.code)
@@ -15,20 +17,16 @@ const SIZE = PRIVATE_TAG_SIZE + KEY_SIZE + PUBLIC_TAG_SIZE + KEY_SIZE
export const PUB_KEY_OFFSET = PRIVATE_TAG_SIZE + KEY_SIZE
-/**
- * @typedef {API.Signer<"key", typeof Signature.EdDSA> & Uint8Array & { verifier: API.Verifier<"key", typeof Signature.EdDSA> }} Signer
- */
-
/**
* Generates new issuer by generating underlying ED25519 keypair.
- * @returns {Promise}
+ * @returns {Promise}
*/
export const generate = () => derive(ED25519.utils.randomPrivateKey())
/**
* Derives issuer from 32 byte long secret key.
* @param {Uint8Array} secret
- * @returns {Promise}
+ * @returns {Promise}
*/
export const derive = async secret => {
if (secret.byteLength !== KEY_SIZE) {
@@ -49,9 +47,21 @@ export const derive = async secret => {
return signer
}
+/**
+ * @param {API.SignerArchive>} archive
+ * @returns {API.EdSigner}
+ */
+export const from = archive => {
+ if (archive instanceof Uint8Array) {
+ return decode(archive)
+ } else {
+ throw new Error(`Unsupported archive format`)
+ }
+}
+
/**
* @param {Uint8Array} bytes
- * @returns {Signer}
+ * @returns {API.EdSigner}
*/
export const decode = bytes => {
if (bytes.byteLength !== SIZE) {
@@ -80,31 +90,40 @@ export const decode = bytes => {
}
/**
- * @param {Signer} signer
- * @return {API.ByteView}
+ * @param {API.EdSigner} signer
+ * @return {API.ByteView}
*/
-export const encode = signer => signer
+export const encode = signer => signer.toArchive()
/**
* @template {string} Prefix
- * @param {Signer} signer
+ * @param {API.EdSigner} signer
* @param {API.MultibaseEncoder} [encoder]
*/
-export const format = (signer, encoder) => (encoder || base64pad).encode(signer)
+export const format = (signer, encoder) =>
+ (encoder || base64pad).encode(encode(signer))
/**
* @template {string} Prefix
* @param {string} principal
* @param {API.MultibaseDecoder} [decoder]
- * @returns {Signer}
+ * @returns {API.EdSigner}
*/
export const parse = (principal, decoder) =>
decode((decoder || base64pad).decode(principal))
/**
- * @implements {API.Signer<'key', typeof Signature.EdDSA>}
+ * @implements {API.EdSigner}
*/
class Ed25519Signer extends Uint8Array {
+ /** @type {typeof code} */
+ get code() {
+ return code
+ }
+ get signer() {
+ return this
+ }
+ /** @type {API.EdVerifier} */
get verifier() {
const bytes = new Uint8Array(this.buffer, PRIVATE_TAG_SIZE + KEY_SIZE)
const verifier = Verifier.decode(bytes)
@@ -149,6 +168,15 @@ class Ed25519Signer extends Uint8Array {
return Signature.create(this.signatureCode, raw)
}
+ /**
+ * @template T
+ * @param {API.ByteView} payload
+ * @param {API.Signature} signature
+ */
+
+ verify(payload, signature) {
+ return this.verifier.verify(payload, signature)
+ }
get signatureAlgorithm() {
return 'EdDSA'
@@ -156,4 +184,8 @@ class Ed25519Signer extends Uint8Array {
get signatureCode() {
return Signature.EdDSA
}
+
+ toArchive() {
+ return this
+ }
}
diff --git a/packages/principal/src/ed25519/type.js b/packages/principal/src/ed25519/type.js
new file mode 100644
index 00000000..e69de29b
diff --git a/packages/principal/src/ed25519/type.ts b/packages/principal/src/ed25519/type.ts
new file mode 100644
index 00000000..1b2f4117
--- /dev/null
+++ b/packages/principal/src/ed25519/type.ts
@@ -0,0 +1,24 @@
+import { Signer, Verifier, ByteView, UCAN, Await } from '@ucanto/interface'
+import * as Signature from '@ipld/dag-ucan/signature'
+
+export * from '@ucanto/interface'
+
+type CODE = typeof Signature.EdDSA
+type ALG = 'EdDSA'
+
+export interface EdSigner
+ extends Signer,
+ UCAN.Verifier {
+ readonly signer: EdSigner
+ readonly verifier: EdVerifier
+
+ readonly code: 0x1300
+ toArchive(): ByteView>
+}
+
+export interface EdVerifier
+ extends Verifier {
+ readonly code: 0xed
+ readonly signatureCode: CODE
+ readonly signatureAlgorithm: ALG
+}
diff --git a/packages/principal/src/ed25519/verifier.js b/packages/principal/src/ed25519/verifier.js
index 158eae13..776bb186 100644
--- a/packages/principal/src/ed25519/verifier.js
+++ b/packages/principal/src/ed25519/verifier.js
@@ -1,13 +1,14 @@
import * as DID from '@ipld/dag-ucan/did'
import * as ED25519 from '@noble/ed25519'
import { varint } from 'multiformats'
-import * as API from '@ucanto/interface'
+import * as API from './type.js'
import * as Signature from '@ipld/dag-ucan/signature'
import { base58btc } from 'multiformats/bases/base58'
export const code = 0xed
+export const name = 'Ed25519'
export const signatureCode = Signature.EdDSA
-export const name = 'Ed25519'
+export const signatureAlgorithm = 'EdDSA'
const PUBLIC_TAG_SIZE = varint.encodingLength(code)
const SIZE = 32 + PUBLIC_TAG_SIZE
@@ -27,7 +28,7 @@ export const parse = did => decode(DID.parse(did))
* corresponding `Principal` that can be used to verify signatures.
*
* @param {Uint8Array} bytes
- * @returns {Verifier}
+ * @returns {API.EdVerifier}
*/
export const decode = bytes => {
const [algorithm] = varint.decode(bytes)
@@ -40,11 +41,7 @@ export const decode = bytes => {
`Expected Uint8Array with byteLength ${SIZE}, instead got Uint8Array with byteLength ${bytes.byteLength}`
)
} else {
- return new Ed25519Principal(
- bytes.buffer,
- bytes.byteOffset,
- bytes.byteLength
- )
+ return new Ed25519Verifier(bytes.buffer, bytes.byteOffset, bytes.byteLength)
}
}
@@ -64,10 +61,21 @@ export const format = principal => DID.format(principal)
export const encode = principal => DID.encode(principal)
/**
- * @implements {API.Verifier<"key", typeof Signature.EdDSA>}
- * @implements {API.Principal<"key">}
+ * @implements {API.EdVerifier}
*/
-class Ed25519Principal extends Uint8Array {
+class Ed25519Verifier extends Uint8Array {
+ /** @type {typeof code} */
+ get code() {
+ return code
+ }
+ /** @type {typeof signatureCode} */
+ get signatureCode() {
+ return signatureCode
+ }
+ /** @type {typeof signatureAlgorithm} */
+ get signatureAlgorithm() {
+ return signatureAlgorithm
+ }
/**
* Raw public key without a multiformat code.
*
diff --git a/packages/principal/src/lib.js b/packages/principal/src/lib.js
index ecee9f3c..dc83ca89 100644
--- a/packages/principal/src/lib.js
+++ b/packages/principal/src/lib.js
@@ -1 +1,9 @@
-export * as ed25519 from './ed25519.js'
+import * as ed25519 from './ed25519.js'
+import * as RSA from './rsa.js'
+import { create as createVerifier } from './verifier.js'
+import { create as createSigner } from './signer.js'
+
+export const Verifier = createVerifier([ed25519.Verifier, RSA.Verifier])
+export const Signer = createSigner([ed25519, RSA])
+
+export { ed25519, RSA }
diff --git a/packages/principal/src/multiformat.js b/packages/principal/src/multiformat.js
new file mode 100644
index 00000000..fca5c851
--- /dev/null
+++ b/packages/principal/src/multiformat.js
@@ -0,0 +1,35 @@
+import { varint } from 'multiformats'
+
+/**
+ *
+ * @param {number} code
+ * @param {Uint8Array} bytes
+ */
+export const tagWith = (code, bytes) => {
+ const offset = varint.encodingLength(code)
+ const multiformat = new Uint8Array(bytes.byteLength + offset)
+ varint.encodeTo(code, multiformat, 0)
+ multiformat.set(bytes, offset)
+
+ return multiformat
+}
+
+/**
+ * @param {number} code
+ * @param {Uint8Array} source
+ * @param {number} byteOffset
+ * @returns
+ */
+export const untagWith = (code, source, byteOffset = 0) => {
+ const bytes = byteOffset !== 0 ? source.subarray(byteOffset) : source
+ const [tag, size] = varint.decode(bytes)
+ if (tag !== code) {
+ throw new Error(
+ `Expected multiformat with 0x${code.toString(
+ 16
+ )} tag instead got 0x${tag.toString(16)}`
+ )
+ } else {
+ return new Uint8Array(bytes.buffer, bytes.byteOffset + size)
+ }
+}
diff --git a/packages/principal/src/rsa.js b/packages/principal/src/rsa.js
new file mode 100644
index 00000000..32363aa8
--- /dev/null
+++ b/packages/principal/src/rsa.js
@@ -0,0 +1,321 @@
+import { webcrypto } from 'one-webcrypto'
+import { base58btc } from 'multiformats/bases/base58'
+import * as API from './rsa/type.js'
+import * as DID from '@ipld/dag-ucan/did'
+import { tagWith, untagWith } from './multiformat.js'
+import * as Signature from '@ipld/dag-ucan/signature'
+import * as SPKI from './rsa/spki.js'
+import * as PKCS8 from './rsa/pkcs8.js'
+import * as PrivateKey from './rsa/private-key.js'
+import * as PublicKey from './rsa/public-key.js'
+export * from './rsa/type.js'
+
+export const name = 'RSA'
+export const code = 0x1305
+const verifierCode = 0x1205
+
+export const signatureCode = Signature.RS256
+export const signatureAlgorithm = 'RS256'
+
+const ALG = 'RSASSA-PKCS1-v1_5'
+const HASH_ALG = 'SHA-256'
+const KEY_SIZE = 2048
+const SALT_LEGNTH = 128
+const IMPORT_PARAMS = {
+ name: ALG,
+ hash: { name: HASH_ALG },
+}
+
+/**
+ * @param {object} options
+ * @param {number} [options.size]
+ * @param {boolean} [options.extractable]
+ * @returns {Promise}
+ */
+export const generate = async ({
+ size = KEY_SIZE,
+ extractable = false,
+} = {}) => {
+ // We start by generate an RSA keypair using web crypto API.
+ const { publicKey, privateKey } = await webcrypto.subtle.generateKey(
+ {
+ name: ALG,
+ modulusLength: size,
+ publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
+ hash: { name: HASH_ALG },
+ },
+
+ extractable,
+ ['sign', 'verify']
+ )
+
+ // Next we need to encode public key, because `RSAVerifier` uses it to
+ // for implementing a `did()` method. To do this we first export
+ // Subject Public Key Info (SPKI) using web crypto API.
+ const spki = await webcrypto.subtle.exportKey('spki', publicKey)
+ // Then we extract public key from the SPKI and tag it with RSA public key
+ // multicode
+ const publicBytes = tagWith(verifierCode, SPKI.decode(new Uint8Array(spki)))
+ // Now that we have publicKey and it's multiformat representation we can
+ // create a verifier.
+ const verifier = new RSAVerifier({ bytes: publicBytes, publicKey })
+
+ // If we generated non extractable key we just wrap actual keys and verifier
+ // in the RSASigner view.
+ if (!extractable) {
+ return new UnextractableRSASigner({
+ privateKey,
+ verifier,
+ })
+ }
+ // Otherwise we export key in Private Key Cryptography Standards (PKCS)
+ // format and extract a bytes corresponding to the private key, which
+ // we tag with RSA private key multiformat code. With both binary and actual
+ // key representation we create a RSASigner view.
+ // Please note that do key export flow during generation so that we can:
+ // 1. Guarantee that it will be exportable.
+ // 2. Make `export` method sync.
+ else {
+ const pkcs8 = await webcrypto.subtle.exportKey('pkcs8', privateKey)
+ const bytes = tagWith(code, PKCS8.decode(new Uint8Array(pkcs8)))
+ return new ExtractableRSASigner({
+ privateKey,
+ bytes,
+ verifier,
+ })
+ }
+}
+
+/**
+ * @param {API.SignerArchive>} archive
+ * @returns {API.RSASigner}
+ */
+export const from = archive => {
+ if (archive instanceof Uint8Array) {
+ return decode(archive)
+ } else {
+ return new UnextractableRSASigner({
+ privateKey: archive.key,
+ verifier: RSAVerifier.parse(archive.did),
+ })
+ }
+}
+
+/**
+ * @param {EncodedSigner} bytes
+ * @returns {API.RSASigner}
+ */
+export const decode = bytes => {
+ // First we decode RSA key data from the private key with multicode tag.
+ const rsa = PrivateKey.decode(untagWith(code, bytes))
+ // Then we encode RSA key data as public key with multicode tag.
+ const publicBytes = tagWith(verifierCode, PublicKey.encode(rsa))
+
+ return new ExtractableRSASigner({
+ bytes,
+ privateKey: webcrypto.subtle.importKey(
+ 'pkcs8',
+ PKCS8.encode(untagWith(code, bytes)),
+ IMPORT_PARAMS,
+ true,
+ ['sign']
+ ),
+
+ verifier: RSAVerifier.decode(publicBytes),
+ })
+}
+
+/**
+ * @implements {API.RSAVerifier}
+ */
+class RSAVerifier {
+ /**
+ * @param {object} options
+ * @param {API.Await} options.publicKey
+ * @param {API.ByteView} options.bytes
+ */
+ constructor({ publicKey, bytes }) {
+ /** @private */
+ this.publicKey = publicKey
+ /** @private */
+ this.bytes = bytes
+ }
+
+ /**
+ * @param {API.ByteView} bytes
+ * @returns {API.RSAVerifier}
+ */
+ static decode(bytes) {
+ return new this({
+ bytes,
+ publicKey: webcrypto.subtle.importKey(
+ 'spki',
+ SPKI.encode(untagWith(verifierCode, bytes)),
+ IMPORT_PARAMS,
+ true,
+ ['verify']
+ ),
+ })
+ }
+ /**
+ * @param {API.DID} did
+ * @returns {API.RSAVerifier}
+ */
+ static parse(did) {
+ return RSAVerifier.decode(/** @type {Uint8Array} */ (DID.parse(did)))
+ }
+
+ /** @type {typeof verifierCode} */
+ get code() {
+ return verifierCode
+ }
+ /**
+ * @type {typeof signatureCode}
+ */
+ get signatureCode() {
+ return signatureCode
+ }
+ /**
+ * @type {typeof signatureAlgorithm}
+ */
+ get signatureAlgorithm() {
+ return signatureAlgorithm
+ }
+ /**
+ * DID of the Principal in `did:key` format.
+ * @returns {API.DID<"key">}
+ */
+ did() {
+ return `did:key:${base58btc.encode(this.bytes)}`
+ }
+
+ /**
+ * @template T
+ * @param {API.ByteView} payload
+ * @param {API.Signature} signature
+ * @returns {Promise}
+ */
+ async verify(payload, signature) {
+ // if signature code does not match RS256 it's not signed by corresponding
+ // signer.
+ if (signature.code !== signatureCode) {
+ return false
+ }
+
+ return webcrypto.subtle.verify(
+ { name: ALG, hash: { name: HASH_ALG } },
+ await this.publicKey,
+ signature.raw,
+ payload
+ )
+ }
+}
+
+/** @type {API.PrincipalParser} */
+export const Verifier = RSAVerifier
+
+/**
+ * @typedef {API.ByteView>} EncodedSigner
+ */
+
+class RSASigner {
+ /**
+ * @param {object} options
+ * @param {API.Await} options.privateKey
+ * @param {API.RSAVerifier} options.verifier
+ */
+ constructor({ privateKey, verifier }) {
+ /** @readonly */
+ this.verifier = verifier
+ /** @protected */
+ this.privateKey = privateKey
+ }
+ get signer() {
+ // @ts-expect-error - we define export methods on subclasses
+ return /** @type {API.RSASigner} */ (this)
+ }
+ /**
+ * @type {typeof code}
+ */
+ get code() {
+ return code
+ }
+ /**
+ * @type {typeof signatureCode}
+ */
+ get signatureCode() {
+ return signatureCode
+ }
+ /**
+ * @type {typeof signatureAlgorithm}
+ */
+ get signatureAlgorithm() {
+ return signatureAlgorithm
+ }
+
+ did() {
+ return this.verifier.did()
+ }
+ /**
+ * @template T
+ * @param {API.ByteView} payload
+ * @param {API.Signature} signature
+ */
+ verify(payload, signature) {
+ return this.verifier.verify(payload, signature)
+ }
+ /**
+ * @template T
+ * @param {API.ByteView} payload
+ * @returns {Promise>}
+ */
+ async sign(payload) {
+ const buffer = await webcrypto.subtle.sign(
+ { name: ALG, saltLength: SALT_LEGNTH },
+ await this.privateKey,
+ payload
+ )
+
+ return Signature.create(signatureCode, new Uint8Array(buffer))
+ }
+}
+
+/**
+ * @implements {API.RSASigner}
+ */
+class ExtractableRSASigner extends RSASigner {
+ /**
+ * @param {object} options
+ * @param {API.Await} options.privateKey
+ * @param {EncodedSigner} options.bytes
+ * @param {API.RSAVerifier} options.verifier
+ */
+ constructor(options) {
+ super(options)
+ this.bytes = options.bytes
+ }
+ toArchive() {
+ return this.bytes
+ }
+}
+
+/**
+ * @implements {API.RSASigner}
+ */
+class UnextractableRSASigner extends RSASigner {
+ /**
+ * @param {object} options
+ * @param {CryptoKey} options.privateKey
+ * @param {API.RSAVerifier} options.verifier
+ */
+ constructor(options) {
+ super(options)
+ this.privateKey = options.privateKey
+ }
+ toArchive() {
+ return {
+ did: this.did(),
+ key: this.privateKey,
+ }
+ }
+}
diff --git a/packages/principal/src/rsa/asn1.js b/packages/principal/src/rsa/asn1.js
new file mode 100644
index 00000000..16c290f3
--- /dev/null
+++ b/packages/principal/src/rsa/asn1.js
@@ -0,0 +1,329 @@
+/**
+ * ASN1 Tags as per https://luca.ntop.org/Teaching/Appunti/asn1.html
+ */
+const TAG_SIZE = 1
+export const INT_TAG = 0x02
+export const BITSTRING_TAG = 0x03
+export const OCTET_STRING_TAG = 0x04
+export const NULL_TAG = 0x05
+export const OBJECT_TAG = 0x06
+export const SEQUENCE_TAG = 0x30
+
+export const UNUSED_BIT_PAD = 0x00
+
+/**
+ * @param {number} length
+ * @returns {Uint8Array}
+ */
+export const encodeDERLength = length => {
+ if (length <= 127) {
+ return new Uint8Array([length])
+ }
+
+ /** @type {number[]} */
+ const octets = []
+ while (length !== 0) {
+ octets.push(length & 0xff)
+ length = length >>> 8
+ }
+ octets.reverse()
+ return new Uint8Array([0x80 | (octets.length & 0xff), ...octets])
+}
+
+/**
+ * @param {Uint8Array} bytes
+ * @param {number} offset
+ * @returns {{number: number, consumed: number}}
+ */
+export const readDERLength = (bytes, offset = 0) => {
+ if ((bytes[offset] & 0x80) === 0) {
+ return { number: bytes[offset], consumed: 1 }
+ }
+
+ const numberBytes = bytes[offset] & 0x7f
+ /* c8 ignore next 5 */
+ if (bytes.length < numberBytes + 1) {
+ throw new Error(
+ `ASN parsing error: Too few bytes. Expected encoded length's length to be at least ${numberBytes}`
+ )
+ }
+
+ let length = 0
+ for (let i = 0; i < numberBytes; i++) {
+ length = length << 8
+ length = length | bytes[offset + i + 1]
+ }
+
+ return { number: length, consumed: numberBytes + 1 }
+}
+
+/**
+ * @param {Uint8Array} input
+ * @param {number} expectedTag
+ * @param {number} position
+ * @returns {number}
+ */
+export const skip = (input, expectedTag, position) => {
+ const parsed = into(input, expectedTag, position)
+ return parsed.position + parsed.length
+}
+
+/**
+ * @param {Uint8Array} input
+ * @param {number} expectedTag
+ * @param {number} offset
+ * @returns {{ position: number, length: number }}
+ */
+export const into = (input, expectedTag, offset) => {
+ const actualTag = input[offset]
+ /* c8 ignore next 7 */
+ if (actualTag !== expectedTag) {
+ throw new Error(
+ `ASN parsing error: Expected tag 0x${expectedTag.toString(
+ 16
+ )} at position ${offset}, but got 0x${actualTag.toString(16)}.`
+ )
+ }
+
+ // length
+ const length = readDERLength(input, offset + TAG_SIZE)
+ const position = offset + TAG_SIZE + length.consumed
+
+ // content
+ return { position, length: length.number }
+}
+
+/**
+ * @param {Uint8Array} input
+ */
+export const encodeBitString = input => {
+ // encode input length + 1 for unused bit pad
+ const length = encodeDERLength(input.byteLength + 1)
+ // allocate a buffer of desired size
+ const bytes = new Uint8Array(
+ TAG_SIZE + // ASN_BITSTRING_TAG
+ length.byteLength +
+ 1 + // amount of unused bits at the end of our bitstring
+ input.byteLength
+ )
+
+ let byteOffset = 0
+ // write bytestring tag
+ bytes[byteOffset] = BITSTRING_TAG
+ byteOffset += TAG_SIZE
+
+ // write length of the bytestring
+ bytes.set(length, byteOffset)
+ byteOffset += length.byteLength
+
+ // write unused bits at the end of our bitstring
+ bytes[byteOffset] = UNUSED_BIT_PAD
+ byteOffset += 1
+
+ // write actual data into bitstring
+ bytes.set(input, byteOffset)
+
+ return bytes
+}
+
+/**
+ * @param {Uint8Array} input
+ */
+export const encodeOctetString = input => {
+ // encode input length
+ const length = encodeDERLength(input.byteLength)
+ // allocate a buffer of desired size
+ const bytes = new Uint8Array(TAG_SIZE + length.byteLength + input.byteLength)
+
+ let byteOffset = 0
+ // write octet string tag
+ bytes[byteOffset] = OCTET_STRING_TAG
+ byteOffset += TAG_SIZE
+
+ // write octet string length
+ bytes.set(length, byteOffset)
+ byteOffset += length.byteLength
+
+ // write actual data into bitstring
+ bytes.set(input, byteOffset)
+
+ return bytes
+}
+
+/**
+ * @param {Uint8Array[]} sequence
+ */
+export const encodeSequence = sequence => {
+ // calculate bytelength for all the parts
+ let byteLength = 0
+ for (const item of sequence) {
+ byteLength += item.byteLength
+ }
+
+ // encode sequence byte length
+ const length = encodeDERLength(byteLength)
+
+ // allocate the buffer to write sequence into
+ const bytes = new Uint8Array(TAG_SIZE + length.byteLength + byteLength)
+
+ let byteOffset = 0
+
+ // write the sequence tag
+ bytes[byteOffset] = SEQUENCE_TAG
+ byteOffset += TAG_SIZE
+
+ // write sequence length
+ bytes.set(length, byteOffset)
+ byteOffset += length.byteLength
+
+ // write each item in the sequence
+ for (const item of sequence) {
+ bytes.set(item, byteOffset)
+ byteOffset += item.byteLength
+ }
+
+ return bytes
+}
+
+/**
+ * @param {Uint8Array} bytes
+ * @param {number} offset
+ */
+export const readSequence = (bytes, offset = 0) => {
+ const { position, length } = into(bytes, SEQUENCE_TAG, offset)
+
+ return new Uint8Array(bytes.buffer, bytes.byteOffset + position, length)
+}
+
+/**
+ * @param {Uint8Array} input
+ */
+export const encodeInt = input => {
+ const extra = input.byteLength === 0 || input[0] & 0x80 ? 1 : 0
+
+ // encode input length
+ const length = encodeDERLength(input.byteLength + extra)
+ // allocate a buffer of desired size
+ const bytes = new Uint8Array(
+ TAG_SIZE + // INT_TAG
+ length.byteLength +
+ input.byteLength +
+ extra
+ )
+
+ let byteOffset = 0
+ // write octet string tag
+ bytes[byteOffset] = INT_TAG
+ byteOffset += TAG_SIZE
+
+ // write int length
+ bytes.set(length, byteOffset)
+ byteOffset += length.byteLength
+
+ // add 0 if the most-significant bit is set
+ if (extra > 0) {
+ bytes[byteOffset] = UNUSED_BIT_PAD
+ byteOffset += extra
+ }
+
+ // write actual data into bitstring
+ bytes.set(input, byteOffset)
+
+ return bytes
+}
+
+/**
+ * @param {Uint8Array} bytes
+ * @param {number} offset
+ * @returns {number}
+ */
+
+export const enterSequence = (bytes, offset = 0) =>
+ into(bytes, SEQUENCE_TAG, offset).position
+
+/**
+ * @param {Uint8Array} bytes
+ * @param {number} offset
+ * @returns {number}
+ */
+export const skipSequence = (bytes, offset = 0) =>
+ skip(bytes, SEQUENCE_TAG, offset)
+
+/**
+ * @param {Uint8Array} bytes
+ * @param {number} offset
+ * @returns {number}
+ */
+export const skipInt = (bytes, offset = 0) => skip(bytes, INT_TAG, offset)
+
+/**
+ * @param {Uint8Array} bytes
+ * @param {number} offset
+ * @returns {Uint8Array}
+ */
+export const readBitString = (bytes, offset = 0) => {
+ const { position, length } = into(bytes, BITSTRING_TAG, offset)
+ const tag = bytes[position]
+ /* c8 ignore next 5 */
+ if (tag !== UNUSED_BIT_PAD) {
+ throw new Error(
+ `Can not read bitstring, expected length to be multiple of 8, but got ${tag} unused bits in last byte.`
+ )
+ }
+
+ return new Uint8Array(
+ bytes.buffer,
+ bytes.byteOffset + position + 1,
+ length - 1
+ )
+}
+
+/**
+ * @param {Uint8Array} bytes
+ * @param {number} byteOffset
+ * @returns {Uint8Array}
+ */
+export const readInt = (bytes, byteOffset = 0) => {
+ const { position, length } = into(bytes, INT_TAG, byteOffset)
+ let delta = 0
+
+ // drop leading 0s
+ while (bytes[position + delta] === 0) {
+ delta++
+ }
+
+ return new Uint8Array(
+ bytes.buffer,
+ bytes.byteOffset + position + delta,
+ length - delta
+ )
+}
+
+/**
+ * @param {Uint8Array} bytes
+ * @param {number} offset
+ * @returns {Uint8Array}
+ */
+export const readOctetString = (bytes, offset = 0) => {
+ const { position, length } = into(bytes, OCTET_STRING_TAG, offset)
+
+ return new Uint8Array(bytes.buffer, bytes.byteOffset + position, length)
+}
+
+/**
+ * @typedef {(bytes:Uint8Array, offset:number) => Uint8Array} Reader
+ * @param {[Reader, ...Reader[]]} readers
+ * @param {Uint8Array} source
+ * @param {number} byteOffset
+ */
+export const readSequenceWith = (readers, source, byteOffset = 0) => {
+ const results = []
+ const sequence = readSequence(source, byteOffset)
+ let offset = 0
+ for (const read of readers) {
+ const chunk = read(sequence, offset)
+ results.push(chunk)
+ offset = chunk.byteOffset + chunk.byteLength - sequence.byteOffset
+ }
+ return results
+}
diff --git a/packages/principal/src/rsa/pkcs8.js b/packages/principal/src/rsa/pkcs8.js
new file mode 100644
index 00000000..a712f31a
--- /dev/null
+++ b/packages/principal/src/rsa/pkcs8.js
@@ -0,0 +1,52 @@
+import * as API from '@ucanto/interface'
+import { base64url } from 'multiformats/bases/base64'
+import {
+ encodeSequence,
+ encodeOctetString,
+ enterSequence,
+ skipSequence,
+ skipInt,
+ readOctetString,
+} from './asn1.js'
+
+const PKSC8_HEADER = new Uint8Array([
+ // version
+ 2, 1, 0,
+ // privateKeyAlgorithm
+ 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0,
+])
+/**
+ * @typedef {import('./private-key').RSAPrivateKey} RSAPrivateKey
+ * @typedef {object} AlgorithmIdentifier
+ * @property {Uint8Array} version
+ * @property {Uint8Array} parameters
+ *
+ * @see https://datatracker.ietf.org/doc/html/rfc5208#section-5
+ * @typedef {object} PrivateKeyInfo
+ * @property {API.ByteView} version
+ * @property {API.ByteView} privateKeyAlgorithm
+ * @property {API.ByteView} privateKey
+ * @property {API.ByteView} [attributes]
+ */
+
+/**
+ * @param {API.ByteView} info
+ * @returns {API.ByteView}
+ */
+export const decode = info => {
+ let offset = 0
+ // go into the top-level SEQUENCE
+ offset = enterSequence(info, offset)
+ offset = skipInt(info, offset)
+ offset = skipSequence(info, offset)
+
+ // we expect the bitstring next
+ return readOctetString(info, offset)
+}
+
+/**
+ * @param {API.ByteView} key
+ * @returns {API.ByteView}
+ */
+export const encode = key =>
+ encodeSequence([PKSC8_HEADER, encodeOctetString(key)])
diff --git a/packages/principal/src/rsa/private-key.js b/packages/principal/src/rsa/private-key.js
new file mode 100644
index 00000000..5a2563ca
--- /dev/null
+++ b/packages/principal/src/rsa/private-key.js
@@ -0,0 +1,127 @@
+import * as API from '@ucanto/interface'
+import { encodeSequence, readInt, readSequenceWith, encodeInt } from './asn1.js'
+import { base64url } from 'multiformats/bases/base64'
+import * as PKCS8 from './pkcs8.js'
+import * as SPKI from './spki.js'
+import * as PublicKey from './public-key.js'
+
+export const code = 0x1305
+const VERSION = new Uint8Array()
+
+/**
+ * @see https://datatracker.ietf.org/doc/html/rfc3447#appendix-A.1.2
+ * @typedef {object} RSAPrivateKey
+ * @property {Uint8Array} v
+ * @property {Uint8Array} n
+ * @property {Uint8Array} e
+ * @property {Uint8Array} d
+ * @property {Uint8Array} p
+ * @property {Uint8Array} q
+ * @property {Uint8Array} dp
+ * @property {Uint8Array} dq
+ * @property {Uint8Array} qi
+ */
+
+/**
+ * Takes private-key information in [Private-Key Information Syntax](https://datatracker.ietf.org/doc/html/rfc5208#section-5)
+ * and extracts all the fields as per [RSA private key syntax](https://datatracker.ietf.org/doc/html/rfc3447#appendix-A.1.2)
+ *
+ *
+ * @param {API.ByteView} source
+ * @param {number} byteOffset
+ * @returns {RSAPrivateKey}
+ */
+export const decode = (source, byteOffset = 0) => {
+ const [v, n, e, d, p, q, dp, dq, qi] = readSequenceWith(
+ [
+ readInt,
+ readInt,
+ readInt,
+ readInt,
+ readInt,
+ readInt,
+ readInt,
+ readInt,
+ readInt,
+ ],
+ source,
+ byteOffset
+ )
+
+ return { v, n, e, d, p, q, dp, dq, qi }
+}
+
+/**
+ * @param {RSAPrivateKey} key
+ * @returns {API.ByteView}
+ */
+export const encode = ({ v, n, e, d, p, q, dp, dq, qi }) => {
+ return encodeSequence([
+ encodeInt(v),
+ encodeInt(n),
+ encodeInt(e),
+ encodeInt(d),
+ encodeInt(p),
+ encodeInt(q),
+ encodeInt(dp),
+ encodeInt(dq),
+ encodeInt(qi),
+ ])
+}
+
+/**
+ * @param {RSAPrivateKey} key
+ * @returns {JsonWebKey}
+ */
+export const toJWK = ({ n, e, d, p, q, dp, dq, qi }) => ({
+ kty: 'RSA',
+ alg: 'RS256',
+ key_ops: ['sign'],
+ ext: true,
+ n: base64url.baseEncode(n),
+ e: base64url.baseEncode(e),
+ d: base64url.baseEncode(d),
+ p: base64url.baseEncode(p),
+ q: base64url.baseEncode(q),
+ dp: base64url.baseEncode(dp),
+ dq: base64url.baseEncode(dq),
+ qi: base64url.baseEncode(qi),
+})
+
+/**
+ * @param {JsonWebKey} key
+ * @returns {RSAPrivateKey}
+ */
+export const fromJWK = ({ n, e, d, p, q, dp, dq, qi }) => ({
+ v: VERSION,
+ n: base64urlDecode(n),
+ e: base64urlDecode(e),
+ d: base64urlDecode(d),
+ p: base64urlDecode(p),
+ q: base64urlDecode(q),
+ dp: base64urlDecode(dp),
+ dq: base64urlDecode(dq),
+ qi: base64urlDecode(qi),
+})
+
+/**
+ * @param {RSAPrivateKey} key
+ */
+export const toPKCS8 = key => PKCS8.encode(encode(key))
+
+/**
+ * @param {API.ByteView} info
+ */
+export const fromPKCS8 = info => decode(PKCS8.decode(info))
+
+/**
+ * @param {RSAPrivateKey} key
+ */
+export const toSPKI = key => SPKI.encode(PublicKey.encode(key))
+
+/**
+ *
+ * @param {string|undefined} input
+ * @returns
+ */
+const base64urlDecode = (input = '') => base64url.baseDecode(input)
diff --git a/packages/principal/src/rsa/public-key.js b/packages/principal/src/rsa/public-key.js
new file mode 100644
index 00000000..b27f020b
--- /dev/null
+++ b/packages/principal/src/rsa/public-key.js
@@ -0,0 +1,70 @@
+import * as API from '@ucanto/interface'
+import { encodeSequence, readInt, encodeInt, readSequenceWith } from './asn1.js'
+import * as SPKI from './spki.js'
+import { base64url } from 'multiformats/bases/base64'
+/**
+ * RSA public key represenatation
+ * @see https://datatracker.ietf.org/doc/html/rfc3447#appendix-A.1
+ *
+ * @typedef {object} RSAPublicKey
+ * @property {API.ByteView} n
+ * @property {API.ByteView} e
+ */
+
+/**
+ * Takes private-key information in [Private-Key Information Syntax](https://datatracker.ietf.org/doc/html/rfc5208#section-5)
+ * and extracts all the fields as per [RSA private key syntax](https://datatracker.ietf.org/doc/html/rfc3447#appendix-A.1.2)
+ *
+ *
+ * @param {API.ByteView} key
+ * @param {number} byteOffset
+ * @returns {RSAPublicKey}
+ */
+export const decode = (key, byteOffset = 0) => {
+ const [n, e] = readSequenceWith([readInt, readInt], key, byteOffset)
+
+ return { n, e }
+}
+
+/**
+ * @param {RSAPublicKey} key
+ * @returns {API.ByteView}
+ */
+export const encode = ({ n, e }) => encodeSequence([encodeInt(n), encodeInt(e)])
+
+/**
+ * @param {RSAPublicKey} key
+ */
+export const toSPKI = key => SPKI.encode(encode(key))
+
+/**
+ * @param {API.ByteView} info
+ */
+export const fromSPKI = info => decode(SPKI.decode(info))
+
+/**
+ * @param {RSAPublicKey} key
+ * @returns {JsonWebKey}
+ */
+export const toJWK = ({ n, e }) => ({
+ kty: 'RSA',
+ alg: 'RS256',
+ key_ops: ['verify'],
+ ext: true,
+ n: base64url.baseEncode(n),
+ e: base64url.baseEncode(e),
+})
+
+/**
+ * @param {JsonWebKey} jwk
+ * @returns {RSAPublicKey}
+ */
+export const fromJWK = ({ n, e }) => ({
+ n: base64urlDecode(n),
+ e: base64urlDecode(e),
+})
+
+/**
+ * @param {string|undefined} input
+ */
+const base64urlDecode = (input = '') => base64url.baseDecode(input)
diff --git a/packages/principal/src/rsa/spki.js b/packages/principal/src/rsa/spki.js
new file mode 100644
index 00000000..221dbc53
--- /dev/null
+++ b/packages/principal/src/rsa/spki.js
@@ -0,0 +1,66 @@
+import * as API from '@ucanto/interface'
+import {
+ encodeSequence,
+ encodeBitString,
+ enterSequence,
+ skipSequence,
+ readBitString,
+} from './asn1.js'
+
+/**
+ * @typedef {import('./public-key.js').RSAPublicKey} RSAPublicKey
+ */
+/**
+ * Described in RFC 5208 Section 4.1: https://tools.ietf.org/html/rfc5280#section-4.1
+ * ```
+ * SubjectPublicKeyInfo ::= SEQUENCE {
+ * algorithm AlgorithmIdentifier,
+ * subjectPublicKey BIT STRING }
+ * ```
+ *
+ * @typedef {object} SubjectPublicKeyInfo
+ * @property {API.ByteView} algorithm
+ * @property {API.ByteView} subjectPublicKey
+ * @typedef {import('./pkcs8.js').AlgorithmIdentifier} AlgorithmIdentifier
+ */
+
+/**
+ * The ASN.1 DER encoded header that needs to be added to an
+ * ASN.1 DER encoded RSAPublicKey to make it a SubjectPublicKeyInfo.
+ *
+ * This byte sequence is always the same.
+ *
+ * A human-readable version of this as part of a dumpasn1 dump:
+ *
+ * SEQUENCE {
+ * OBJECT IDENTIFIER rsaEncryption (1 2 840 113549 1 1 1)
+ * NULL
+ * }
+ *
+ * See https://github.com/ucan-wg/ts-ucan/issues/30
+ */
+export const SPKI_PARAMS_ENCODED = new Uint8Array([
+ 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0,
+])
+
+/**
+ * @param {API.ByteView} key
+ * @returns {API.ByteView}
+ */
+export const encode = key =>
+ encodeSequence([SPKI_PARAMS_ENCODED, encodeBitString(key)])
+
+/**
+ *
+ * @param {API.ByteView} info
+ * @returns {API.ByteView}
+ */
+export const decode = info => {
+ // go into the top-level SEQUENCE
+ const offset = enterSequence(info, 0)
+ // skip the header we expect (SKPI_PARAMS_ENCODED)
+ const keyOffset = skipSequence(info, offset)
+
+ // we expect the bitstring next
+ return readBitString(info, keyOffset)
+}
diff --git a/packages/principal/src/rsa/type.js b/packages/principal/src/rsa/type.js
new file mode 100644
index 00000000..e69de29b
diff --git a/packages/principal/src/rsa/type.ts b/packages/principal/src/rsa/type.ts
new file mode 100644
index 00000000..7061661d
--- /dev/null
+++ b/packages/principal/src/rsa/type.ts
@@ -0,0 +1,22 @@
+import { Signer, Verifier, ByteView, UCAN, Await } from '@ucanto/interface'
+
+export * from '@ucanto/interface'
+
+type CODE = 0xd01205
+type ALG = 'RS256'
+
+export interface RSASigner
+ extends Signer,
+ UCAN.Verifier {
+ readonly signer: RSASigner
+ readonly verifier: RSAVerifier
+
+ readonly code: 0x1305
+}
+
+export interface RSAVerifier
+ extends Verifier {
+ readonly code: 0x1205
+ readonly signatureCode: CODE
+ readonly signatureAlgorithm: ALG
+}
diff --git a/packages/principal/src/signer.js b/packages/principal/src/signer.js
new file mode 100644
index 00000000..8296f251
--- /dev/null
+++ b/packages/principal/src/signer.js
@@ -0,0 +1,24 @@
+import * as API from '@ucanto/interface'
+
+/**
+ * @template {[API.SignerImporter, ...API.SignerImporter[]]} Importers
+ * @param {Importers} importers
+ */
+export const create = importers => {
+ const from = /** @type {API.Intersection} */ (
+ /**
+ * @param {API.SignerArchive} archive
+ * @returns {API.Signer}
+ */
+ archive => {
+ for (const importer of importers) {
+ try {
+ return importer.from(archive)
+ } catch (_) {}
+ }
+ throw new Error(`Unsupported signer`)
+ }
+ )
+
+ return { create, from }
+}
diff --git a/packages/principal/src/verifier.js b/packages/principal/src/verifier.js
new file mode 100644
index 00000000..e3371685
--- /dev/null
+++ b/packages/principal/src/verifier.js
@@ -0,0 +1,20 @@
+import * as API from '@ucanto/interface'
+
+/**
+ * @param {API.PrincipalParser[]} options
+ */
+export const create = options => ({
+ create,
+ /**
+ * @param {API.DID} did
+ * @return {API.Verifier}
+ */
+ parse: did => {
+ for (const option of options) {
+ try {
+ return option.parse(did)
+ } catch (_) {}
+ }
+ throw new Error(`Unsupported principal with DID ${did}`)
+ },
+})
diff --git a/packages/principal/test/ed25519.spec.js b/packages/principal/test/ed25519.spec.js
new file mode 100644
index 00000000..8dc14602
--- /dev/null
+++ b/packages/principal/test/ed25519.spec.js
@@ -0,0 +1,186 @@
+import { ed25519, ed25519 as Lib } from '../src/lib.js'
+import { assert } from 'chai'
+import { sha256 } from 'multiformats/hashes/sha2'
+import { varint } from 'multiformats'
+
+describe('signing principal', () => {
+ const { Signer } = Lib
+
+ it('exports', () => {
+ assert.equal(Lib.code, 0x1300)
+ assert.equal(Lib.name, 'Ed25519')
+ assert.equal(Lib.signatureAlgorithm, 'EdDSA')
+ assert.equal(Lib.signatureCode, 0xd0ed)
+ assert.equal(typeof Lib.derive, 'function')
+ assert.equal(typeof Lib.generate, 'function')
+
+ assert.equal(typeof Lib.Verifier, 'object')
+ assert.equal(typeof Lib.Signer, 'object')
+ })
+
+ it('generate', async () => {
+ const signer = await Lib.generate()
+ assert.ok(signer.did().startsWith('did:key'))
+ assert.equal(signer.code, 0x1300)
+ assert.equal(signer.signatureCode, 0xd0ed)
+ assert.equal(signer.signatureAlgorithm, 'EdDSA')
+ assert.equal(signer.signer, signer)
+ assert.equal(signer.verifier.code, 0xed)
+ assert.equal(signer.verifier.signatureCode, 0xd0ed)
+ assert.equal(signer.verifier.signatureAlgorithm, 'EdDSA')
+
+ const payload = await sha256.encode(new TextEncoder().encode('hello world'))
+ const signature = await signer.sign(payload)
+
+ const verifier = Lib.Verifier.parse(signer.did())
+ assert.equal(
+ await verifier.verify(payload, signature),
+ true,
+ 'signer can verify signature'
+ )
+ assert.equal(await signer.verify(payload, signature), true)
+
+ assert.equal(signer.signatureAlgorithm, 'EdDSA')
+ assert.equal(signer.signatureCode, 0xd0ed)
+ assert.equal(signer.did(), verifier.did())
+ })
+
+ it('derive', async () => {
+ const original = await Lib.generate()
+ // @ts-expect-error - secret is not defined by interface
+ const derived = await Lib.derive(original.secret)
+
+ // @ts-expect-error - secret is not defined by interface
+ assert.deepEqual(original.secret, derived.secret)
+ assert.equal(original.did(), derived.did())
+ })
+
+ it('derive throws on bad input', async () => {
+ // @ts-expect-error - secret is not defined by interface
+ const { secret } = await Lib.generate()
+ try {
+ await Lib.derive(secret.subarray(1))
+ assert.fail('Expected to throw')
+ } catch (error) {
+ assert.match(String(error), /Expected Uint8Array with byteLength of 32/)
+ }
+ })
+
+ it('SigningPrincipal.decode', async () => {
+ const signer = await Lib.generate()
+ const bytes = Signer.encode(signer)
+
+ assert.deepEqual(Signer.decode(signer.toArchive()), signer)
+
+ const invalid = new Uint8Array(signer.toArchive())
+ varint.encodeTo(4, invalid, 0)
+ assert.throws(() => Signer.decode(invalid), /must be a multiformat with/)
+
+ assert.throws(
+ () => Signer.decode(signer.toArchive().slice(0, 32)),
+ /Expected Uint8Array with byteLength/
+ )
+
+ const malformed = new Uint8Array(signer.toArchive())
+ // @ts-ignore
+ varint.encodeTo(4, malformed, Signer.PUB_KEY_OFFSET)
+
+ assert.throws(() => Signer.decode(malformed), /must contain public key/)
+ })
+
+ it('SigningPrincipal decode encode roundtrip', async () => {
+ const signer = await Lib.generate()
+
+ assert.deepEqual(Signer.decode(Signer.encode(signer)), signer)
+ })
+
+ it('SigningPrincipal.format', async () => {
+ const signer = await Lib.generate()
+
+ assert.deepEqual(Signer.parse(Signer.format(signer)), signer)
+ })
+
+ it('SigningPrincipal.did', async () => {
+ const signer = await Lib.generate()
+
+ assert.equal(signer.did().startsWith('did:key:'), true)
+ })
+})
+
+describe('principal', () => {
+ const { Verifier, Signer } = Lib
+
+ it('exports', async () => {
+ assert.equal(Verifier, await import('../src/ed25519/verifier.js'))
+ assert.equal(Verifier.code, 0xed)
+ assert.equal(Verifier.signatureAlgorithm, 'EdDSA')
+ })
+
+ it('Verifier.parse', async () => {
+ const signer = await Lib.generate()
+ const verifier = Verifier.parse(signer.did())
+ const bytes = signer.toArchive()
+
+ assert.deepEqual(
+ new Uint8Array(bytes.buffer, bytes.byteOffset + Signer.PUB_KEY_OFFSET),
+ Object(verifier)
+ )
+ assert.equal(verifier.did(), signer.did())
+ })
+
+ it('Verifier.decode', async () => {
+ const signer = await Lib.generate()
+ const bytes = signer.toArchive()
+
+ const verifier = new Uint8Array(
+ bytes.buffer,
+ bytes.byteOffset + Signer.PUB_KEY_OFFSET
+ )
+ assert.deepEqual(Object(Verifier.decode(verifier)), verifier)
+ assert.throws(
+ () => Verifier.decode(signer.toArchive()),
+ /key algorithm with multicode/
+ )
+
+ assert.throws(
+ () => Verifier.decode(verifier.slice(0, 32)),
+ /Expected Uint8Array with byteLength/
+ )
+ })
+
+ it('Verifier.format', async () => {
+ const signer = await Lib.generate()
+ const verifier = Verifier.parse(signer.did())
+
+ assert.deepEqual(Verifier.format(verifier), signer.did())
+ })
+
+ it('Verifier.encode', async () => {
+ const { verifier } = await Lib.generate()
+
+ const bytes = Verifier.encode(verifier)
+ assert.deepEqual(Verifier.decode(bytes), verifier)
+ })
+
+ it('signer toArchive', async () => {
+ const signer = await Lib.generate()
+
+ assert.equal(signer.toArchive(), Signer.encode(signer))
+ })
+
+ it('can parse keys with forward slash', async () => {
+ // @see https://github.com/web3-storage/ucanto/issues/85
+ const key =
+ 'MgCYY9lYduqC9rrtD1YvZzcEfPCFBaYsTe0T+8RLLBawPWu0BAaNqeI86jQPsOeSaZ7p+ZPWGFqggfvSMFw+AJ7BH8/U='
+ const ed = ed25519.parse(key)
+ assert.equal(
+ ed.did(),
+ 'did:key:z6MkeZeyji49ZVbinyPENzhZMVML7s79bbjN9K4iNFBsFkdr'
+ )
+
+ assert.equal(ed25519.format(ed), key)
+
+ const payload = new TextEncoder().encode('hello world')
+ assert.equal(await ed.verify(payload, await ed.sign(payload)), true)
+ })
+})
diff --git a/packages/principal/test/lib.spec.js b/packages/principal/test/lib.spec.js
index 80a23a6e..749d7993 100644
--- a/packages/principal/test/lib.spec.js
+++ b/packages/principal/test/lib.spec.js
@@ -1,148 +1,84 @@
-import { ed25519 as Lib } from '../src/lib.js'
+import * as API from '@ucanto/interface'
+import { Verifier, Signer, ed25519, RSA } from '../src/lib.js'
import { assert } from 'chai'
-import { sha256 } from 'multiformats/hashes/sha2'
-import { varint } from 'multiformats'
-describe('signing principal', () => {
- const { Signer } = Lib
+const utf8 = new TextEncoder()
+describe('PrincipalParser', () => {
+ it('parse & verify', async () => {
+ const ed = await ed25519.generate()
+ const rsa = await RSA.generate()
- it('exports', () => {
- assert.equal(Lib.name, 'Ed25519')
- assert.equal(Lib.code, 0x1300)
- assert.equal(typeof Lib.derive, 'function')
- assert.equal(typeof Lib.generate, 'function')
+ const edp = Verifier.parse(ed.did())
- assert.equal(typeof Lib.Verifier, 'object')
- assert.equal(typeof Lib.Signer, 'object')
- })
-
- it('generate', async () => {
- const signer = await Lib.generate()
- assert.ok(signer.did().startsWith('did:key'))
- assert.ok(signer instanceof Uint8Array)
-
- const payload = await sha256.encode(new TextEncoder().encode('hello world'))
- const signature = await signer.sign(payload)
-
- const verifier = Lib.Verifier.parse(signer.did())
- assert.ok(
- await verifier.verify(payload, signature),
- 'signer can verify signature'
- )
-
- assert.equal(signer.signatureAlgorithm, 'EdDSA')
- assert.equal(signer.signatureCode, 0xd0ed)
- })
+ const payload = utf8.encode('hello ed')
- it('derive', async () => {
- const original = await Lib.generate()
- // @ts-expect-error - secret is not defined by interface
- const derived = await Lib.derive(original.secret)
+ assert.equal(await edp.verify(payload, await ed.sign(payload)), true)
+ assert.equal(await edp.verify(payload, await rsa.sign(payload)), false)
- // @ts-expect-error - secret is not defined by interface
- assert.deepEqual(original.secret, derived.secret)
- assert.equal(original.did(), derived.did())
+ const rsap = Verifier.parse(rsa.did())
+ assert.equal(await rsap.verify(payload, await ed.sign(payload)), false)
+ assert.equal(await rsap.verify(payload, await rsa.sign(payload)), true)
})
- it('derive throws on bad input', async () => {
- // @ts-expect-error - secret is not defined by interface
- const { secret } = await Lib.generate()
- try {
- await Lib.derive(secret.subarray(1))
- assert.fail('Expected to throw')
- } catch (error) {
- assert.match(String(error), /Expected Uint8Array with byteLength of 32/)
- }
- })
-
- it('SigningPrincipal.decode', async () => {
- const signer = await Lib.generate()
- const bytes = Signer.encode(signer)
-
- assert.deepEqual(Signer.decode(signer), signer)
-
- const invalid = new Uint8Array(signer)
- varint.encodeTo(4, invalid, 0)
- assert.throws(() => Signer.decode(invalid), /must be a multiformat with/)
-
+ it('throws on unknown did', () => {
assert.throws(
- () => Signer.decode(signer.slice(0, 32)),
- /Expected Uint8Array with byteLength/
+ () => Verifier.parse('did:echo:boom'),
+ /Unsupported principal/
)
-
- const malformed = new Uint8Array(signer)
- // @ts-ignore
- varint.encodeTo(4, malformed, Signer.PUB_KEY_OFFSET)
-
- assert.throws(() => Signer.decode(malformed), /must contain public key/)
- })
-
- it('SigningPrincipal decode encode roundtrip', async () => {
- const signer = await Lib.generate()
-
- assert.deepEqual(Signer.decode(Signer.encode(signer)), signer)
})
- it('SigningPrincipal.format', async () => {
- const signer = await Lib.generate()
+ it('throws on invalid ed archive', async () => {
+ const ed = await ed25519.generate()
+ const rsa = await RSA.generate()
- assert.deepEqual(Signer.parse(Signer.format(signer)), signer)
- })
+ const { key } = /** @type {API.SignerInfo} */ (rsa.toArchive())
- it('SigningPrincipal.did', async () => {
- const signer = await Lib.generate()
+ const archive = { did: ed.did(), key }
- assert.equal(signer.did().startsWith('did:key:'), true)
+ assert.throws(() => Signer.from(archive), /Unsupported signer/)
})
-})
-describe('principal', () => {
- const { Verifier, Signer } = Lib
+ it('ed decode & sign', async () => {
+ const ed = await ed25519.generate()
- it('exports', async () => {
- assert.equal(Verifier, await import('../src/ed25519/verifier.js'))
- assert.equal(Verifier.code, 0xed)
- assert.equal(Verifier.name, 'Ed25519')
- })
-
- it('Verifier.parse', async () => {
- const signer = await Lib.generate()
- const verifier = Verifier.parse(signer.did())
+ const bytes = ed.toArchive()
+ const signer = Signer.from(bytes)
+ const payload = utf8.encode('hello ed')
- assert.deepEqual(
- new Uint8Array(signer.buffer, signer.byteOffset + Signer.PUB_KEY_OFFSET),
- verifier
+ const signature = await signer.sign(payload)
+ assert.equal(
+ await ed.verify(
+ payload,
+ /** @type {API.Signature} */ (
+ signature
+ )
+ ),
+ true
)
- assert.equal(verifier.did(), signer.did())
})
- it('Verifier.decode', async () => {
- const signer = await Lib.generate()
+ it('rsa decode & sign', async () => {
+ const rsa = await RSA.generate({ extractable: true })
- const verifier = new Uint8Array(
- signer.buffer,
- signer.byteOffset + Signer.PUB_KEY_OFFSET
- )
- assert.deepEqual(Verifier.decode(verifier), verifier)
- assert.throws(() => Verifier.decode(signer), /key algorithm with multicode/)
+ const signer = Signer.from(rsa.toArchive())
+ const payload = utf8.encode('hello ed')
- assert.throws(
- () => Verifier.decode(verifier.slice(0, 32)),
- /Expected Uint8Array with byteLength/
+ const signature = await signer.sign(payload)
+ assert.equal(
+ await rsa.verify(
+ payload,
+ /** @type {API.Signature} */ (
+ signature
+ )
+ ),
+ true
)
})
- it('Verifier.format', async () => {
- const signer = await Lib.generate()
- const verifier = Verifier.parse(signer.did())
-
- assert.deepEqual(Verifier.format(verifier), signer.did())
- })
-
- it('Verifier.encode', async () => {
- const { verifier } = await Lib.generate()
-
- const bytes = Verifier.encode(verifier)
- assert.deepEqual(Verifier.decode(bytes), verifier)
+ it('throws on unknown signer', () => {
+ assert.throws(
+ () => Signer.from(new Uint8Array([1, 1, 1])),
+ /Unsupported signer/
+ )
})
})
diff --git a/packages/principal/test/rsa.spec.js b/packages/principal/test/rsa.spec.js
new file mode 100644
index 00000000..93e86268
--- /dev/null
+++ b/packages/principal/test/rsa.spec.js
@@ -0,0 +1,330 @@
+import * as RSA from '../src/rsa.js'
+import * as PrivateKey from '../src/rsa/private-key.js'
+import * as PublicKey from '../src/rsa/public-key.js'
+import * as PKCS8 from '../src/rsa/pkcs8.js'
+import * as multiformat from '../src/multiformat.js'
+import { assert } from 'chai'
+import { varint } from 'multiformats'
+import { webcrypto } from 'one-webcrypto'
+
+export const utf8 = new TextEncoder()
+describe('RSA', () => {
+ it('can generate non extractabel keypair', async () => {
+ const signer = await RSA.generate()
+
+ assert.equal(signer.code, 0x1305)
+ assert.equal(signer.signatureCode, 0xd01205)
+ assert.equal(signer.signatureAlgorithm, 'RS256')
+ assert.match(signer.did(), /did:key:/)
+ assert.equal(typeof signer.toArchive, 'function')
+ assert.equal(typeof signer.verify, 'function')
+ assert.equal(typeof signer.sign, 'function')
+
+ assert.equal(signer.signer, signer)
+
+ const { verifier } = signer
+ assert.equal(typeof verifier.verify, 'function')
+ assert.equal(verifier.code, 0x1205)
+ assert.equal(verifier.signatureCode, 0xd01205)
+ assert.equal(verifier.signatureAlgorithm, 'RS256')
+ assert.equal(verifier.did(), signer.did())
+
+ const { key, did } = /** @type {RSA.SignerInfo} */ (signer.toArchive())
+ assert.equal(did, signer.did())
+ assert.equal(key.type, 'private')
+ assert.deepEqual(Object(key.algorithm), {
+ name: 'RSASSA-PKCS1-v1_5',
+ modulusLength: 2048,
+ publicExponent: new Uint8Array([1, 0, 1]),
+ hash: {
+ name: 'SHA-256',
+ },
+ })
+ assert.equal(key.extractable, false)
+ assert.deepEqual(key.usages, ['sign'])
+ })
+
+ it('can archive 🔁 restore unextractable', async () => {
+ const original = await RSA.generate()
+ const bytes = original.toArchive()
+ const restored = RSA.from(bytes)
+ const payload = utf8.encode('hello world')
+
+ assert.equal(
+ await restored.verify(payload, await original.sign(payload)),
+ true
+ )
+
+ assert.equal(
+ await original.verify(payload, await restored.sign(payload)),
+ true
+ )
+ })
+
+ it('can generate extractable keypair', async () => {
+ const signer = await RSA.generate({ extractable: true })
+ assert.equal(signer.signatureCode, 0xd01205)
+ assert.equal(signer.signatureAlgorithm, 'RS256')
+ assert.match(signer.did(), /did:key:/)
+ assert.equal(signer, signer.signer)
+ assert.equal(typeof signer.toArchive, 'function')
+ assert.equal(typeof signer.sign, 'function')
+ assert.equal(typeof signer.verify, 'function')
+
+ assert.equal(signer.signer, signer)
+
+ const { verifier } = signer
+ assert.equal(typeof verifier.verify, 'function')
+ assert.equal(verifier.code, 0x1205)
+ assert.equal(verifier.signatureCode, 0xd01205)
+ assert.equal(verifier.signatureAlgorithm, 'RS256')
+ assert.equal(verifier.did(), signer.did())
+
+ const bytes = signer.toArchive()
+ if (!(bytes instanceof Uint8Array)) {
+ return assert.fail()
+ }
+ assert.deepEqual([0x1305, 2], varint.decode(bytes))
+
+ /** @type {CryptoKey} */
+ // @ts-expect-error - field is private
+ const privateKey = signer.privateKey
+ assert.equal(privateKey.type, 'private')
+ assert.deepEqual(Object(privateKey.algorithm), {
+ name: 'RSASSA-PKCS1-v1_5',
+ modulusLength: 2048,
+ publicExponent: new Uint8Array([1, 0, 1]),
+ hash: {
+ name: 'SHA-256',
+ },
+ })
+ assert.equal(privateKey.extractable, true)
+ assert.deepEqual(privateKey.usages, ['sign'])
+ })
+
+ it('can archive 🔁 restore extractable', async () => {
+ const original = await RSA.generate({ extractable: true })
+ const restored = RSA.from(original.toArchive())
+ const payload = utf8.encode('hello world')
+
+ assert.equal(
+ await restored.verify(payload, await original.sign(payload)),
+ true
+ )
+
+ assert.equal(
+ await original.verify(payload, await restored.sign(payload)),
+ true
+ )
+ })
+
+ it('can sign & verify', async () => {
+ const signer = await RSA.generate()
+ const payload = utf8.encode('hello world')
+
+ const signature = await signer.sign(payload)
+ assert.equal(signature.code, signer.signatureCode)
+ assert.equal(signature.algorithm, signer.signatureAlgorithm)
+
+ const { verifier } = signer
+ assert.equal(await verifier.verify(payload, signature), true)
+ assert.equal(await signer.verify(payload, signature), true)
+ })
+
+ it('can parse verifier', async () => {
+ const principal = await RSA.generate()
+ const payload = utf8.encode('hello world')
+ const verifier = RSA.Verifier.parse(principal.did())
+
+ const signature = await principal.sign(payload)
+ assert.equal(await verifier.verify(payload, signature), true)
+ })
+
+ it('can format / parse verifier', async () => {
+ const { signer, verifier: original } = await RSA.generate()
+
+ const did = await original.did()
+ const parsed = RSA.Verifier.parse(did)
+ const payload = utf8.encode('hello world')
+
+ const signature = await signer.sign(payload)
+ assert.equal(await original.verify(payload, signature), true)
+ assert.equal(await parsed.verify(payload, signature), true)
+ assert.deepEqual(did, await parsed.did())
+ })
+
+ it('can parse', async () => {
+ const did =
+ 'did:key:z4MXj1wBzi9jUstyPMS4jQqB6KdJaiatPkAtVtGc6bQEQEEsKTic4G7Rou3iBf9vPmT5dbkm9qsZsuVNjq8HCuW1w24nhBFGkRE4cd2Uf2tfrB3N7h4mnyPp1BF3ZttHTYv3DLUPi1zMdkULiow3M1GfXkoC6DoxDUm1jmN6GBj22SjVsr6dxezRVQc7aj9TxE7JLbMH1wh5X3kA58H3DFW8rnYMakFGbca5CB2Jf6CnGQZmL7o5uJAdTwXfy2iiiyPxXEGerMhHwhjTA1mKYobyk2CpeEcmvynADfNZ5MBvcCS7m3XkFCMNUYBS9NQ3fze6vMSUPsNa6GVYmKx2x6JrdEjCk3qRMMmyjnjCMfR4pXbRMZa3i'
+
+ const verifier = RSA.Verifier.parse(did)
+ assert.deepEqual(verifier.did(), did)
+
+ const payload = utf8.encode('hello world')
+ const signer = await RSA.generate({ extractable: true })
+ const signature = await signer.sign(payload)
+
+ assert.equal(await verifier.verify(payload, signature), false)
+ })
+
+ it('can not verify other signatures', async () => {
+ const signer = await RSA.generate()
+ const payload = utf8.encode('hello world')
+ const signature = await signer.sign(payload)
+
+ assert.equal(await signer.verify(payload, signature), true)
+
+ assert.equal(
+ await signer.verify(payload, {
+ ...signature,
+ // @ts-expect-error
+ code: signature.code + 1,
+ }),
+ false
+ )
+ })
+})
+
+/**
+ * @param {Exclude} format
+ * @param {RSA.RSASigner|RSA.RSAVerifier} principal
+ */
+const exportKey = async (format, principal) => {
+ // @ts-expect-error - accessing private keys
+ const cryptoKey = principal.privateKey || principal.publicKey
+ return webcrypto.subtle.exportKey(format, cryptoKey)
+}
+
+describe('PrivateKey', () => {
+ it('PublicKey fromSPKI 🔁 toSPKI', async () => {
+ const { verifier } = await RSA.generate()
+ const spki = new Uint8Array(await exportKey('spki', verifier))
+ const key = PublicKey.fromSPKI(spki)
+ assert.deepEqual(spki, PublicKey.toSPKI(key))
+ })
+
+ it('PKCS8 decode 🔁 encode', async () => {
+ const { signer } = await RSA.generate({ extractable: true })
+ const expected = new Uint8Array(await exportKey('pkcs8', signer))
+ const key = PKCS8.decode(expected)
+ const actual = PKCS8.encode(key)
+
+ assert.deepEqual(actual, expected)
+ })
+
+ it('PrivateKey decode 🔁 encode', async () => {
+ const { signer } = await RSA.generate({ extractable: true })
+ const pkcs8 = new Uint8Array(await exportKey('pkcs8', signer))
+ const source = PKCS8.decode(pkcs8)
+
+ const key = PrivateKey.decode(source)
+ const bytes = PrivateKey.encode(key)
+
+ assert.deepEqual(source, bytes)
+ })
+
+ it('PrivateKey fromPKCS8 🔁 toPKCS8', async () => {
+ const { signer } = await RSA.generate({ extractable: true })
+ const pkcs8 = new Uint8Array(await exportKey('pkcs8', signer))
+ const key = PrivateKey.fromPKCS8(pkcs8)
+ const info = PrivateKey.toPKCS8(key)
+ assert.deepEqual(info, pkcs8)
+ })
+})
+
+it('multiformat', () => {
+ const value = multiformat.tagWith(
+ 5,
+ multiformat.tagWith(4, new Uint8Array([1, 1, 1]))
+ )
+
+ const outer = multiformat.untagWith(5, value)
+ assert.deepEqual(
+ {
+ buffer: value.buffer,
+ byteOffset: 1,
+ byteLength: value.byteLength - 1,
+ },
+ {
+ buffer: outer.buffer,
+ byteOffset: outer.byteOffset,
+ byteLength: outer.byteLength,
+ }
+ )
+ assert.deepEqual(outer, value.subarray(1))
+
+ const inner = multiformat.untagWith(4, value, 1)
+ assert.deepEqual(
+ {
+ buffer: value.buffer,
+ byteOffset: 2,
+ byteLength: value.byteLength - 2,
+ },
+ {
+ buffer: inner.buffer,
+ byteOffset: inner.byteOffset,
+ byteLength: inner.byteLength,
+ }
+ )
+ assert.deepEqual(inner, value.subarray(2))
+
+ assert.throws(
+ () => multiformat.untagWith(3, value),
+ /Expected multiformat with 0x3 tag instead got 0x5/
+ )
+})
+
+it('toJWK 🔁 fromJWK', async () => {
+ const { signer, verifier } = await RSA.generate({ extractable: true })
+
+ /** @type {CryptoKeyPair} */
+ const keyPair = {
+ // @ts-expect-error - accessing private field
+ privateKey: signer.privateKey,
+ // @ts-expect-error - accessing private field
+ publicKey: verifier.publicKey,
+ }
+
+ const jwk = await webcrypto.subtle.exportKey('jwk', keyPair.privateKey)
+
+ const privateKey = PrivateKey.decode(
+ multiformat.untagWith(
+ RSA.code,
+ /** @type {Uint8Array} */ (signer.toArchive())
+ )
+ )
+
+ assert.deepEqual(PrivateKey.fromJWK(jwk), privateKey)
+ assert.deepEqual(PrivateKey.toJWK(PrivateKey.fromJWK(jwk)), jwk)
+
+ assert.deepEqual(
+ PublicKey.toJWK(privateKey),
+ await webcrypto.subtle.exportKey('jwk', keyPair.publicKey)
+ )
+
+ const publicKey = PublicKey.decode(
+ multiformat.untagWith(
+ signer.verifier.code,
+ // @ts-expect-error - accessing private property
+ signer.verifier.bytes
+ )
+ )
+
+ assert.deepEqual(PublicKey.fromJWK(jwk), publicKey)
+ assert.deepEqual(PublicKey.fromJWK(PublicKey.toJWK(publicKey)), publicKey)
+ assert.deepEqual(PublicKey.fromJWK(PublicKey.toJWK(privateKey)), publicKey)
+})
+
+it('toSPKI 🔁 fromSPKI', async () => {
+ const signer = await RSA.generate({ extractable: true })
+ const spki = new Uint8Array(await exportKey('spki', signer.verifier))
+
+ const privateKey = PrivateKey.decode(
+ multiformat.untagWith(
+ RSA.code,
+ /** @type {Uint8Array} */ (signer.toArchive())
+ )
+ )
+
+ assert.deepEqual(PrivateKey.toSPKI(privateKey), spki)
+})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c1ee589e..38161565 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -111,6 +111,7 @@ importers:
mocha: ^10.0.0
multiformats: ^9.8.1
nyc: ^15.1.0
+ one-webcrypto: ^1.0.3
playwright-test: ^8.1.1
typescript: ^4.8.3
dependencies:
@@ -118,6 +119,7 @@ importers:
'@noble/ed25519': 1.7.0
'@ucanto/interface': link:../interface
multiformats: 9.8.1
+ one-webcrypto: 1.0.3
devDependencies:
'@types/chai': 4.3.3
'@types/mocha': 9.1.1
@@ -2348,6 +2350,10 @@ packages:
wrappy: 1.0.2
dev: true
+ /one-webcrypto/1.0.3:
+ resolution: {integrity: sha512-fu9ywBVBPx0gS9K0etIROTiCkvI5S1TDjFsYFb3rC1ewFxeOqsbzq7aIMBHsYfrTHBcGXJaONXXjTl8B01cW1Q==}
+ dev: false
+
/onetime/5.1.2:
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
engines: {node: '>=6'}