UNPKG

@ceramicnetwork/stream-handler-common

Version:

Ceramic stream handler common types and utilities

70 lines 3.63 kB
import { getEIP191Verifier } from '@didtools/pkh-ethereum'; import { getSolanaVerifier } from '@didtools/pkh-solana'; import { getStacksVerifier } from '@didtools/pkh-stacks'; import { getTezosVerifier } from '@didtools/pkh-tezos'; import { WebauthnAuth } from '@didtools/key-webauthn'; import { ServiceMetrics as Metrics } from '@ceramicnetwork/observability'; import { NodeMetrics } from '@ceramicnetwork/node-metrics'; import { StreamUtils } from '@ceramicnetwork/common'; const DEFAULT_CACAO_REVOCATION_PHASE_OUT_SECS = 24 * 60 * 60; export const CACAO_EXPIRED = 'cacao_expired'; const verifiersCACAO = { ...getEIP191Verifier(), ...getSolanaVerifier(), ...getStacksVerifier(), ...getTezosVerifier(), ...WebauthnAuth.getVerifier(), }; export class SignatureUtils { static async verifyCommitSignature(commitData, signer, controller, model, streamId) { try { const cacao = await this._verifyCapabilityAuthz(commitData, streamId, model); const atTime = commitData.timestamp ? new Date(commitData.timestamp * 1000) : undefined; await signer.verifyJWS(commitData.envelope, { atTime: atTime, issuer: controller, capability: cacao, revocationPhaseOutSecs: DEFAULT_CACAO_REVOCATION_PHASE_OUT_SECS, verifiers: verifiersCACAO, }); } catch (e) { const original = e.message ? e.message : String(e); if (original.includes('CACAO has expired')) { Metrics.count(CACAO_EXPIRED, 1, { source: 'new_commit' }); NodeMetrics.recordError(CACAO_EXPIRED + '_new_commit'); } throw new Error(`Can not verify signature for commit ${commitData.cid} to stream ${streamId} which has controller DID ${controller}: ${original}`); } } static async _verifyCapabilityAuthz(commitData, streamId, model) { const cacao = commitData.capability; if (!cacao) return null; const resources = cacao.p.resources; const payloadCID = commitData.envelope.link.toString(); if (!resources.includes(`ceramic://*`) && !resources.includes(`ceramic://${streamId.toString()}`) && !resources.includes(`ceramic://${streamId.toString()}?payload=${payloadCID}`) && !(model && resources.includes(`ceramic://*?model=${model.toString()}`))) { throw new Error(`Capability does not have appropriate permissions to update this Stream`); } return cacao; } static checkForCacaoExpiration(state) { const now = Math.floor(Date.now() / 1000); for (const logEntry of state.log) { const timestamp = logEntry.timestamp ?? now; if (!logEntry.expirationTime) { continue; } const expirationTime = logEntry.expirationTime + DEFAULT_CACAO_REVOCATION_PHASE_OUT_SECS; if (expirationTime < timestamp) { Metrics.count(CACAO_EXPIRED, 1, { source: 'existing_state' }); NodeMetrics.recordError(CACAO_EXPIRED + '_existing_state'); throw new Error(`CACAO expired: Commit ${logEntry.cid.toString()} of Stream ${StreamUtils.streamIdFromState(state).toString()} has a CACAO that expired at ${logEntry.expirationTime}. Loading the stream with 'sync: SyncOptions.ALWAYS_SYNC' will restore the stream to a usable state, by discarding the invalid commits (this means losing the data from those invalid writes!)`); } } } } //# sourceMappingURL=signature_utils.js.map