Skip to content

Commit

Permalink
Merge branch 'main' into feat/decoders-dsl
Browse files Browse the repository at this point in the history
* main:
  fix: FetchResponse type (#113)
  feat: implement rsa signer / verifier (#102)
  • Loading branch information
hugomrdias committed Oct 14, 2022
2 parents 62ed88e + 9397eb6 commit 444517f
Show file tree
Hide file tree
Showing 24 changed files with 1,812 additions and 156 deletions.
1 change: 0 additions & 1 deletion .github/workflows/principal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ jobs:
strategy:
matrix:
node-version:
- 14
- 16
os:
- ubuntu-latest
Expand Down
58 changes: 54 additions & 4 deletions packages/interface/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
Signature,
Principal,
Verifier,
Signer,
Signer as UCANSigner,
} from '@ipld/dag-ucan'
import * as UCAN from '@ipld/dag-ucan'
import {
Expand All @@ -39,8 +39,6 @@ export * from './transport.js'
export type {
Transport,
Principal,
Verifier,
Signer,
Phantom,
Tuple,
DID,
Expand Down Expand Up @@ -388,9 +386,61 @@ export type URI<P extends Protocol = Protocol> = `${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<Self extends Signer = Signer> {
from(archive: SignerArchive<Self>): Self
}

export interface Signer<M extends string = string, A extends number = number>
extends UCANSigner<M, A> {
/**
* 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<Signer<M, A>>
}

export interface SignerInfo<Self extends Signer = Signer> {
readonly did: ReturnType<Self['did']>
readonly key: CryptoKey
}

export type SignerArchive<Self extends Signer = Signer> =
| ByteView<Self>
| SignerInfo<Self>

export { Verifier }

export type InferInvokedCapability<
C extends CapabilityParser<Match<ParsedCapability>>
> = C extends CapabilityParser<Match<infer T>> ? T : never

export type Intersection<T> = (T extends any ? (i: T) => void : never) extends (
i: infer I
) => void
? I
: never
10 changes: 9 additions & 1 deletion packages/principal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -52,13 +53,20 @@
],
"ed25519": [
"dist/src/ed25519.d.ts"
],
"rsa": [
"dist/src/rsa.d.ts"
]
}
},
"exports": {
"./ed25519": {
"types": "./dist/src/ed25519.d.ts",
"import": "./src/ed25519.js"
},
"./rsa": {
"types": "./dist/src/rsa.d.ts",
"import": "./src/rsa.js"
}
},
"c8": {
Expand Down
64 changes: 48 additions & 16 deletions packages/principal/src/ed25519/signer.js
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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<Signer>}
* @returns {Promise<API.EdSigner>}
*/
export const generate = () => derive(ED25519.utils.randomPrivateKey())

/**
* Derives issuer from 32 byte long secret key.
* @param {Uint8Array} secret
* @returns {Promise<Signer>}
* @returns {Promise<API.EdSigner>}
*/
export const derive = async secret => {
if (secret.byteLength !== KEY_SIZE) {
Expand All @@ -49,9 +47,21 @@ export const derive = async secret => {
return signer
}

/**
* @param {API.SignerArchive<API.Signer<"key", typeof signatureCode>>} 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) {
Expand Down Expand Up @@ -80,31 +90,40 @@ export const decode = bytes => {
}

/**
* @param {Signer} signer
* @return {API.ByteView<Signer>}
* @param {API.EdSigner} signer
* @return {API.ByteView<API.EdSigner>}
*/
export const encode = signer => signer
export const encode = signer => signer.toArchive()

/**
* @template {string} Prefix
* @param {Signer} signer
* @param {API.EdSigner} signer
* @param {API.MultibaseEncoder<Prefix>} [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<Prefix>} [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)
Expand Down Expand Up @@ -149,11 +168,24 @@ class Ed25519Signer extends Uint8Array {

return Signature.create(this.signatureCode, raw)
}
/**
* @template T
* @param {API.ByteView<T>} payload
* @param {API.Signature<T, typeof this.signatureCode>} signature
*/

verify(payload, signature) {
return this.verifier.verify(payload, signature)
}

get signatureAlgorithm() {
return 'EdDSA'
}
get signatureCode() {
return Signature.EdDSA
}

toArchive() {
return this
}
}
Empty file.
24 changes: 24 additions & 0 deletions packages/principal/src/ed25519/type.ts
Original file line number Diff line number Diff line change
@@ -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<M extends string = 'key'>
extends Signer<M, CODE>,
UCAN.Verifier<M, CODE> {
readonly signer: EdSigner<M>
readonly verifier: EdVerifier<M>

readonly code: 0x1300
toArchive(): ByteView<EdSigner<M>>
}

export interface EdVerifier<M extends string = 'key'>
extends Verifier<M, CODE> {
readonly code: 0xed
readonly signatureCode: CODE
readonly signatureAlgorithm: ALG
}
30 changes: 19 additions & 11 deletions packages/principal/src/ed25519/verifier.js
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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)
Expand All @@ -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)
}
}

Expand All @@ -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.
*
Expand Down
10 changes: 9 additions & 1 deletion packages/principal/src/lib.js
Original file line number Diff line number Diff line change
@@ -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 }
35 changes: 35 additions & 0 deletions packages/principal/src/multiformat.js
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading

0 comments on commit 444517f

Please sign in to comment.