@ceramicnetwork/stream-handler-common
Version:
Ceramic stream handler common types and utilities
70 lines • 3.63 kB
JavaScript
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