UNPKG

@digitalcredentials/ed25519-verification-key-2020

Version:

Javascript library for generating and working with Ed25519VerificationKey2020 key pairs, for use with crypto-ld.

476 lines 19.4 kB
/*! * Copyright (c) 2021 Digital Bazaar, Inc. All rights reserved. */ import { KeyPair } from '@digitalcredentials/keypair'; import { base58btc, base64url } from './baseX.js'; import ed25519 from './ed25519.js'; const SUITE_ID = 'Ed25519VerificationKey2020'; // multibase base58-btc header const MULTIBASE_BASE58BTC_HEADER = 'z'; // multicodec ed25519-pub header as varint const MULTICODEC_ED25519_PUB_HEADER = new Uint8Array([0xed, 0x01]); // multicodec ed25519-priv header as varint const MULTICODEC_ED25519_PRIV_HEADER = new Uint8Array([0x80, 0x26]); export class Ed25519VerificationKey2020 extends KeyPair { // Used by CryptoLD harness's fromKeyId() method. static SUITE_CONTEXT = 'https://w3id.org/security/suites/ed25519-2020/v1'; // Used by CryptoLD harness for dispatching. static suite = SUITE_ID; publicKeyMultibase; privateKeyMultibase; /** * An implementation of the Ed25519VerificationKey2020 spec, for use with * Linked Data Proofs. * * @see https://w3c-ccg.github.io/lds-ed25519-2020/#ed25519verificationkey2020 * @see https://github.com/digitalbazaar/jsonld-signatures * * @param {object} options - Options hashmap. * @param {string} options.controller - Controller DID or document url. * @param {string} [options.id] - The key ID. If not provided, will be * composed of controller and key fingerprint as hash fragment. * @param {string} options.publicKeyMultibase - Multibase encoded public key * with a multicodec ed25519-pub varint header [0xed, 0x01]. * @param {string} [options.privateKeyMultibase] - Multibase private key * with a multicodec ed25519-priv varint header [0x80, 0x26]. * @param {string} [options.revoked] - Timestamp of when the key has been * revoked, in RFC3339 format. If not present, the key itself is considered * not revoked. Note that this mechanism is slightly different than DID * Document key revocation, where a DID controller can revoke a key from * that DID by removing it from the DID Document. */ constructor({ id, controller, revoked, publicKeyMultibase, privateKeyMultibase } = {}) { super({ id, controller, revoked }); this.type = SUITE_ID; if (!publicKeyMultibase) { throw new TypeError('The "publicKeyMultibase" property is required.'); } if (!_isValidKeyHeader(publicKeyMultibase, MULTICODEC_ED25519_PUB_HEADER)) { throw new TypeError('"publicKeyMultibase" has invalid header bytes: ' + `"${publicKeyMultibase}".`); } if (privateKeyMultibase && !_isValidKeyHeader(privateKeyMultibase, MULTICODEC_ED25519_PRIV_HEADER)) { throw new Error('"privateKeyMultibase" has invalid header bytes.'); } // assign valid key values this.publicKeyMultibase = publicKeyMultibase; this.privateKeyMultibase = privateKeyMultibase; // set key identifier if controller is provided if (controller && this.controller && !this.id) { this.id = `${this.controller}#${this.fingerprint()}`; } } /** * Creates an Ed25519 Key Pair from an existing serialized key pair. * * @param {object} options - Key pair options (see constructor). * @example * > const keyPair = await Ed25519VerificationKey2020.from({ * controller: 'did:ex:1234', * type: 'Ed25519VerificationKey2020', * publicKeyMultibase, * privateKeyMultibase * }); * * @returns {Promise<Ed25519VerificationKey2020>} An Ed25519 Key Pair. */ static async from(options) { if (options.type === 'Ed25519VerificationKey2018') { return Ed25519VerificationKey2020.fromEd25519VerificationKey2018({ keyPair: options }); } if (options.type === 'JsonWebKey2020') { return Ed25519VerificationKey2020.fromJsonWebKey2020(options); } return new Ed25519VerificationKey2020(options); } /** * Instance creation method for backwards compatibility with the * `Ed25519VerificationKey2018` key suite. * * @see https://github.com/digitalbazaar/ed25519-verification-key-2018 * @typedef {object} Ed25519VerificationKey2018 * @param {Ed25519VerificationKey2018} keyPair - Ed25519 2018 suite key pair. * * @returns {Ed25519VerificationKey2020} - 2020 suite instance. */ static fromEd25519VerificationKey2018({ keyPair }) { if (!keyPair.publicKeyBase58) { throw new Error('keyPair.publicKeyBase58 property is required.'); } const publicKeyMultibase = _encodeMbKey(MULTICODEC_ED25519_PUB_HEADER, base58btc.decode(keyPair.publicKeyBase58)); const keyPair2020 = new Ed25519VerificationKey2020({ id: keyPair.id, controller: keyPair.controller, publicKeyMultibase }); if (keyPair.privateKeyBase58) { keyPair2020.privateKeyMultibase = _encodeMbKey(MULTICODEC_ED25519_PRIV_HEADER, base58btc.decode(keyPair.privateKeyBase58)); } return keyPair2020; } /** * Creates a key pair instance (public key only) from a JsonWebKey2020 * object. * * @see https://w3c-ccg.github.io/lds-jws2020/#json-web-key-2020 * * @param {object} options - Options hashmap. * @param {string} options.id - Key id. * @param {string} options.type - Key suite type. * @param {string} options.controller - Key controller. * @param {object} options.publicKeyJwk - JWK object. * * @returns {Promise<Ed25519VerificationKey2020>} Resolves with key pair. */ static async fromJsonWebKey2020({ id, type, controller, publicKeyJwk, privateKeyJwk }) { if (type !== 'JsonWebKey2020') { throw new TypeError(`Invalid key type: "${type}".`); } if (!publicKeyJwk) { throw new TypeError('"publicKeyJwk" property is required.'); } const { kty, crv } = publicKeyJwk; if (kty !== 'OKP') { throw new TypeError('"kty" is required to be "OKP".'); } if (crv !== 'Ed25519') { throw new TypeError('"crv" is required to be "Ed25519".'); } const { x: publicKeyBase64Url } = publicKeyJwk; const publicKeyBytes = base64url.decode(publicKeyBase64Url); const publicKeyMultibase = _encodeMbKey(MULTICODEC_ED25519_PUB_HEADER, publicKeyBytes); const inputKeyDocument = { id, controller, publicKeyMultibase }; if (privateKeyJwk) { const { d: privateKeyBase64Url } = privateKeyJwk; const privateKeyBytes = base64url.decode(privateKeyBase64Url); // Concat the private and public key bytes const combinedPrivatePublicBytes = new Uint8Array(privateKeyBytes.length + publicKeyBytes.length); combinedPrivatePublicBytes.set(privateKeyBytes); combinedPrivatePublicBytes.set(publicKeyBytes, privateKeyBytes.length); inputKeyDocument.privateKeyMultibase = _encodeMbKey(MULTICODEC_ED25519_PRIV_HEADER, combinedPrivatePublicBytes); } return Ed25519VerificationKey2020.from(inputKeyDocument); } /** * Generates a KeyPair with an optional deterministic seed. * * @param {object} [options={}] - Options hashmap. * @param {Uint8Array} [options.seed] - A 32-byte array seed for a * deterministic key. * * @returns {Promise<Ed25519VerificationKey2020>} Resolves with generated * public/private key pair. */ static async generate({ seed, ...keyPairOptions } = {}) { let keyObject; if (seed) { keyObject = await ed25519.generateKeyPairFromSeed(seed); } else { keyObject = await ed25519.generateKeyPair(); } const publicKeyMultibase = _encodeMbKey(MULTICODEC_ED25519_PUB_HEADER, keyObject.publicKey); const privateKeyMultibase = _encodeMbKey(MULTICODEC_ED25519_PRIV_HEADER, keyObject.secretKey); return new Ed25519VerificationKey2020({ publicKeyMultibase, privateKeyMultibase, ...keyPairOptions }); } /** * Creates an instance of Ed25519VerificationKey2020 from a key fingerprint. * * @param {object} options - Options hashmap. * @param {string} options.fingerprint - Multibase encoded key fingerprint. * * @returns {Ed25519VerificationKey2020} Returns key pair instance (with * public key only). */ static fromFingerprint({ fingerprint }) { return new Ed25519VerificationKey2020({ publicKeyMultibase: fingerprint }); } /** * @returns {Uint8Array} Public key bytes. */ get _publicKeyBuffer() { if (!this.publicKeyMultibase) { return; } // remove multibase header const publicKeyMulticodec = base58btc.decode(this.publicKeyMultibase.substr(1)); // remove multicodec header const publicKeyBytes = publicKeyMulticodec.slice(MULTICODEC_ED25519_PUB_HEADER.length); return publicKeyBytes; } /** * @returns {Uint8Array} Private key bytes. */ get _privateKeyBuffer() { if (!this.privateKeyMultibase) { return; } // remove multibase header const privateKeyMulticodec = base58btc.decode(this.privateKeyMultibase.substr(1)); // remove multicodec header const privateKeyBytes = privateKeyMulticodec.slice(MULTICODEC_ED25519_PRIV_HEADER.length); return privateKeyBytes; } /** * Generates and returns a multiformats encoded * ed25519 public key fingerprint (for use with cryptonyms, for example). * * @see https://github.com/multiformats/multicodec * * @returns {string} The fingerprint. */ fingerprint() { return this.publicKeyMultibase; } /** * Exports the serialized representation of the KeyPair * and other information that JSON-LD Signatures can use to form a proof. * * @param {object} [options={}] - Options hashmap. * @param {boolean} [options.publicKey] - Export public key material? * @param {boolean} [options.privateKey] - Export private key material? * @param {boolean} [options.includeContext] - Include JSON-LD context? * * @returns {object} A plain js object that's ready for serialization * (to JSON, etc), for use in DIDs, Linked Data Proofs, etc. */ export({ publicKey = false, privateKey = false, includeContext = false } = {}) { if (!(publicKey || privateKey)) { throw new TypeError('Export requires specifying either "publicKey" or "privateKey".'); } const exportedKey = { id: this.id, type: this.type }; if (includeContext) { exportedKey['@context'] = Ed25519VerificationKey2020.SUITE_CONTEXT; } if (this.controller) { exportedKey.controller = this.controller; } if (publicKey) { exportedKey.publicKeyMultibase = this.publicKeyMultibase; } if (privateKey) { exportedKey.privateKeyMultibase = this.privateKeyMultibase; } if (this.revoked) { exportedKey.revoked = this.revoked; } return exportedKey; } /** * Exports the representation of the KeyPair in Ed25519VerificationKey2018 * serialization format. * * @param {object} [options={}] - Options hashmap. * @param {boolean} [options.publicKey] - Export public key material? * @param {boolean} [options.privateKey] - Export private key material? * @param {boolean} [options.includeContext] - Include JSON-LD context? * * @returns {object} A plain js object that's ready for serialization * (to JSON, etc), for use in DIDs, Linked Data Proofs, etc. */ toEd255519VerificationKey2018({ publicKey = false, privateKey = false, includeContext = false } = {}) { if (!(publicKey || privateKey)) { throw new TypeError('Export requires specifying either "publicKey" or "privateKey".'); } const exportedKey = { id: this.id, type: 'Ed25519VerificationKey2018' }; if (includeContext) { exportedKey['@context'] = 'https://w3id.org/security/suites/ed25519-2018/v1'; } if (this.controller) { exportedKey.controller = this.controller; } if (publicKey && this._publicKeyBuffer) { exportedKey.publicKeyBase58 = base58btc.encode(this._publicKeyBuffer); } if (privateKey && this._privateKeyBuffer) { exportedKey.privateKeyBase58 = base58btc.encode(this._privateKeyBuffer); } if (this.revoked) { exportedKey.revoked = this.revoked; } return exportedKey; } /** * Returns the JWK representation of this key pair. * * @see https://datatracker.ietf.org/doc/html/rfc8037 * * @param {object} [options={}] - Options hashmap. * @param {boolean} [options.publicKey] - Include public key? * @param {boolean} [options.privateKey] - Include private key? * * @returns {{kty: string, crv: string, x: string, d: string}} JWK * representation. */ toJwk({ publicKey = true, privateKey = false } = {}) { if (!(publicKey || privateKey)) { throw new TypeError('Either a "publicKey" or a "privateKey" is required.'); } if (!this._publicKeyBuffer) { throw new TypeError('Public key buffer is not set.'); } const jwk = { crv: 'Ed25519', kty: 'OKP' }; if (publicKey && this._publicKeyBuffer) { jwk.x = base64url.encode(this._publicKeyBuffer); } if (privateKey && this._privateKeyBuffer) { // the private key buffer is a concatenation of <priv key bytes><pub key bytes> // however, the JWK wants just the private key jwk.d = base64url.encode(this._privateKeyBuffer.slice(0, this._privateKeyBuffer.length - this._publicKeyBuffer.length)); } return jwk; } /** * @see https://datatracker.ietf.org/doc/html/rfc8037#appendix-A.3 * * @returns {Promise<string>} JWK Thumbprint. */ async jwkThumbprint() { if (!this._publicKeyBuffer) { throw new TypeError('Public key buffer is not set.'); } const publicKey = base64url.encode(this._publicKeyBuffer); const serialized = `{"crv":"Ed25519","kty":"OKP","x":"${publicKey}"}`; const data = new TextEncoder().encode(serialized); return base64url.encode(new Uint8Array(await ed25519.sha256digest(data))); } /** * Returns the JsonWebKey2020 representation of this key pair. * * @see https://w3c-ccg.github.io/lds-jws2020/#json-web-key-2020 * * @returns {Promise<object>} JsonWebKey2020 representation. */ async toJsonWebKey2020() { const serialized = { '@context': 'https://w3id.org/security/jws/v1', type: 'JsonWebKey2020', publicKeyJwk: this.toJwk({ publicKey: true }) }; if (this.controller) { serialized.controller = this.controller; serialized.id = `${this.controller}#${await this.jwkThumbprint()}`; } return serialized; } /** * Tests whether the fingerprint was generated from a given key pair. * * @example * > edKeyPair.verifyFingerprint({fingerprint: 'z6Mk2S2Q...6MkaFJewa'}); * {verified: true}; * * @param {object} options - Options hashmap. * @param {string} options.fingerprint - A public key fingerprint. * * @returns {{valid: boolean, error: *}} Result of verification. */ verifyFingerprint({ fingerprint }) { // fingerprint should have multibase base58-btc header if (fingerprint[0] !== MULTIBASE_BASE58BTC_HEADER) { return { error: new Error('"fingerprint" must be a multibase encoded string.'), verified: false }; } if (!this._publicKeyBuffer) { throw new TypeError('Public key buffer is not set.'); } let fingerprintBuffer; try { fingerprintBuffer = base58btc.decode(fingerprint.substr(1)); if (!fingerprintBuffer) { throw new TypeError('Invalid encoding of fingerprint.'); } } catch (e) { return { error: e, verified: false }; } const buffersEqual = _isEqualBuffer(this._publicKeyBuffer, fingerprintBuffer.slice(2)); // validate the first two multicodec bytes const verified = fingerprintBuffer[0] === MULTICODEC_ED25519_PUB_HEADER[0] && fingerprintBuffer[1] === MULTICODEC_ED25519_PUB_HEADER[1] && buffersEqual; if (!verified) { return { error: new Error('Invalid fingerprint encoding (expecting 0xed01 byte prefix).'), verified: false }; } return { verified }; } signer() { const privateKeyBuffer = this._privateKeyBuffer; return { async sign({ data }) { if (!privateKeyBuffer) { throw new Error('A private key is not available for signing.'); } return ed25519.sign(privateKeyBuffer, data); }, id: this.id }; } verifier() { const publicKeyBuffer = this._publicKeyBuffer; return { async verify({ data, signature }) { if (!publicKeyBuffer) { throw new Error('A public key is not available for verifying.'); } return ed25519.verify(publicKeyBuffer, data, signature); }, id: this.id }; } } // check to ensure that two buffers are byte-for-byte equal // WARNING: this function must only be used to check public information as // timing attacks can be used for non-constant time checks on // secret information. function _isEqualBuffer(buf1, buf2) { if (buf1.length !== buf2.length) { return false; } for (let i = 0; i < buf1.length; i++) { if (buf1[i] !== buf2[i]) { return false; } } return true; } // check a multibase key for an expected header function _isValidKeyHeader(multibaseKey, expectedHeader) { if (!(typeof multibaseKey === 'string' && multibaseKey[0] === MULTIBASE_BASE58BTC_HEADER)) { return false; } const keyBytes = base58btc.decode(multibaseKey.slice(1)); return expectedHeader.every((val, i) => keyBytes[i] === val); } // encode a multibase base58-btc multicodec key function _encodeMbKey(header, key) { const mbKey = new Uint8Array(header.length + key.length); mbKey.set(header); mbKey.set(key, header.length); return MULTIBASE_BASE58BTC_HEADER + base58btc.encode(mbKey); } //# sourceMappingURL=Ed25519VerificationKey2020.js.map