-
Notifications
You must be signed in to change notification settings - Fork 119
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: simple mutability API using IPNS (#648)
This PR implements w3name as specified in https://github.com/alanshaw/w3name with some changes to the DB schema because it turns out IPNS record newness isn't solely determined by sequence number. Demo: <img width="1164" alt="Screenshot 2021-11-17 at 15 27 27" src="https://user-images.githubusercontent.com/152863/142232523-30c117b9-c23c-4cd4-9c33-d426d4dd4a94.png">
- Loading branch information
Alan Shaw
authored
Nov 25, 2021
1 parent
774c5ee
commit 9c287bb
Showing
35 changed files
with
4,844 additions
and
8,513 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' | ||
import { toString as uint8ArrayToString } from 'uint8arrays/to-string' | ||
import * as ipns from './utils/ipns/index.js' | ||
import * as Digest from 'multiformats/hashes/digest' | ||
import { base36 } from 'multiformats/bases/base36' | ||
import { CID } from 'multiformats/cid' | ||
import * as ed25519 from './utils/crypto/ed25519.js' | ||
import { PreciseDate } from '@google-cloud/precise-date' | ||
import { HTTPError } from './errors.js' | ||
import { JSONResponse } from './utils/json-response.js' | ||
|
||
const libp2pKeyCode = 0x72 | ||
|
||
/** | ||
* @param {Request} request | ||
* @param {import('./env').Env} env | ||
*/ | ||
export async function nameGet (request, env) { | ||
const { params: { key } } = request | ||
const { code } = CID.parse(key, base36) | ||
if (code !== libp2pKeyCode) { | ||
throw new HTTPError(`invalid key, expected: ${libp2pKeyCode} codec code but got: ${code}`, 400) | ||
} | ||
|
||
const rawRecord = await env.db.resolveNameRecord(key) | ||
if (!rawRecord) { | ||
throw new HTTPError(`record not found for key: ${key}. Only keys published using the Web3.Storage API may be resolved here.`, 404) | ||
} | ||
|
||
const { value } = ipns.unmarshal(uint8ArrayFromString(rawRecord, 'base64pad')) | ||
|
||
return new JSONResponse({ | ||
value: uint8ArrayToString(value), | ||
record: rawRecord | ||
}) | ||
} | ||
|
||
/** | ||
* @param {Request} request | ||
* @param {import('./env').Env} env | ||
*/ | ||
export async function namePost (request, env) { | ||
const { params: { key } } = request | ||
const keyCid = CID.parse(key, base36) | ||
|
||
if (keyCid.code !== libp2pKeyCode) { | ||
throw new HTTPError(`invalid key code: ${keyCid.code}`, 400) | ||
} | ||
|
||
const record = await request.text() | ||
const entry = ipns.unmarshal(uint8ArrayFromString(record, 'base64pad')) | ||
const pubKey = ed25519.unmarshalPublicKey(Digest.decode(keyCid.multihash.bytes).bytes) | ||
|
||
if (entry.pubKey && !ed25519.unmarshalPublicKey(entry.pubKey).equals(pubKey)) { | ||
throw new HTTPError('embedded public key mismatch', 400) | ||
} | ||
|
||
await ipns.validate(pubKey, entry) | ||
|
||
await env.db.publishNameRecord( | ||
key, | ||
record, | ||
Boolean(entry.signatureV2), | ||
entry.sequence, | ||
new PreciseDate(uint8ArrayToString(entry.validity)).getFullTime() | ||
) | ||
|
||
return new JSONResponse({ id: key }, { status: 202 }) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
/* eslint-env browser */ | ||
import { sha256 } from 'multiformats/hashes/sha2' | ||
import { equals as uint8ArrayEquals } from 'uint8arrays/equals' | ||
import { PublicKey, KeyType } from './pb/keys.js' | ||
import { errCode } from '../err-code.js' | ||
|
||
const PUBLIC_KEY_BYTE_LENGTH = 32 | ||
|
||
const ERROR_NON_ED25519_PUBLIC_KEY = 'ERROR_NON_ED25519_PUBLIC_KEY' | ||
const ERROR_INVALID_KEY_LENGTH = 'ERROR_INVALID_KEY_LENGTH' | ||
|
||
/** | ||
* NODE-ED25519 exists only in the CloudFlare workers runtime API | ||
* see: https://developers.cloudflare.com/workers/runtime-apis/web-crypto#footnote%201 | ||
*/ | ||
const CLOUDFLARE_ED25519 = { | ||
name: 'NODE-ED25519', | ||
namedCurve: 'NODE-ED25519' | ||
} | ||
|
||
/** | ||
* @param {Uint8Array} key Public key to verify with. | ||
* @param {Uint8Array} sig Signature used to sign the data. | ||
* @param {Uint8Array} data Data to verify. | ||
*/ | ||
async function verify (key, sig, data) { | ||
try { | ||
const cryptoKey = await crypto.subtle.importKey('raw', key, CLOUDFLARE_ED25519, false, ['verify']) | ||
return crypto.subtle.verify(CLOUDFLARE_ED25519, cryptoKey, sig, data) | ||
} catch (err) { | ||
if (err instanceof Error && err.name === 'NotSupportedError') { | ||
console.warn('using tweetnacl for ed25519 - you should not see this message when running in the CloudFlare worker runtime') | ||
const { default: nacl } = await import('tweetnacl') | ||
return nacl.sign.detached.verify(data, sig, key) | ||
} | ||
throw err | ||
} | ||
} | ||
|
||
/** | ||
* @param {Uint8Array} buf | ||
*/ | ||
export function unmarshalPublicKey (buf) { | ||
const decoded = PublicKey.decode(buf) | ||
const data = decoded.Data | ||
if (decoded.Type !== KeyType.Ed25519) { | ||
throw errCode(new Error('invalid public key type'), ERROR_NON_ED25519_PUBLIC_KEY) | ||
} | ||
return unmarshalEd25519PublicKey(data) | ||
} | ||
|
||
class Ed25519PublicKey { | ||
constructor (key) { | ||
this._key = ensureKey(key, PUBLIC_KEY_BYTE_LENGTH) | ||
} | ||
|
||
async verify (data, sig) { | ||
return verify(this._key, sig, data) | ||
} | ||
|
||
marshal () { | ||
return this._key | ||
} | ||
|
||
get bytes () { | ||
return PublicKey.encode({ | ||
Type: KeyType.Ed25519, | ||
Data: this.marshal() | ||
}).finish() | ||
} | ||
|
||
equals (key) { | ||
return uint8ArrayEquals(this.bytes, key.bytes) | ||
} | ||
|
||
async hash () { | ||
const { bytes } = await sha256.digest(this.bytes) | ||
return bytes | ||
} | ||
} | ||
|
||
function unmarshalEd25519PublicKey (bytes) { | ||
bytes = ensureKey(bytes, PUBLIC_KEY_BYTE_LENGTH) | ||
return new Ed25519PublicKey(bytes) | ||
} | ||
|
||
/** | ||
* @param {Uint8Array} key | ||
* @param {number} length | ||
*/ | ||
function ensureKey (key, length) { | ||
key = Uint8Array.from(key || []) | ||
if (key.length !== length) { | ||
throw errCode(new Error(`key must be a Uint8Array of length ${length}, got ${key.length}`), ERROR_INVALID_KEY_LENGTH) | ||
} | ||
return key | ||
} |
Oops, something went wrong.