@0xpolygonid/js-sdk
Version:
SDK to work with Polygon ID
226 lines (190 loc) • 7.21 kB
text/typescript
import { BasicMessage, IPacker, JWSPackerParams } from '../types';
import {
AcceptJwsAlgorithms,
MediaType,
ProtocolVersion,
SUPPORTED_PUBLIC_KEY_TYPES
} from '../constants';
import { extractPublicKeyBytes, resolveVerificationMethods } from '../utils/did';
import { keyPath, KMS } from '../../kms/';
import { verifyJWS } from 'did-jwt';
import { DIDDocument, Resolvable, parse } from 'did-resolver';
import { bytesToBase64url } from '../../utils/encoding';
import {
byteDecoder,
byteEncoder,
bytesToHex,
decodeBase64url,
encodeBase64url
} from '../../utils';
import { parseAcceptProfile } from '../utils';
/**
* Packer that can pack message to JWZ token,
* and unpack and validate JWZ envelope
* @public
* @class ZKPPacker
* @implements implements IPacker interface
*/
export class JWSPacker implements IPacker {
private readonly supportedAlgorithms = [AcceptJwsAlgorithms.ES256K, AcceptJwsAlgorithms.ES256KR];
private readonly supportedProtocolVersions = [ProtocolVersion.V1];
/**
* Creates an instance of JWSPacker.
*
* @param {KMS} _kms
* @param {Resolvable} _documentResolver
* @memberof JWSPacker
*/
constructor(private readonly _kms: KMS, private readonly _documentResolver: Resolvable) {}
/**
* Packs the given payload and returns a promise that resolves to the packed data.
*
* @param {Uint8Array} payload - The payload to be packed.
* @param {PackerParams} param - The packing parameters.
* @returns `Promise<Uint8Array>`
*/
packMessage(msg: BasicMessage, param: JWSPackerParams): Promise<Uint8Array> {
return this.packInternal(msg, param);
}
/**
* creates JSON Web Signature token
*
* @param {Uint8Array} payload - serialized message
* @param {PackerParams} params - sender id and proving alg are required
* @returns `Promise<Uint8Array>`
*/
async pack(payload: Uint8Array, params: JWSPackerParams): Promise<Uint8Array> {
const message = JSON.parse(byteDecoder.decode(payload));
return this.packInternal(message, params);
}
/**
* validate envelope which is jwz token
*
* @param {Uint8Array} envelope
* @returns `Promise<BasicMessage>`
*/
async unpack(envelope: Uint8Array): Promise<BasicMessage> {
const jws = byteDecoder.decode(envelope);
const [headerStr, msgStr] = jws.split('.');
const header = JSON.parse(decodeBase64url(headerStr));
const message = JSON.parse(decodeBase64url(msgStr));
const explicitSender = parse(header.kid)?.did;
if (explicitSender && explicitSender !== message.from) {
throw new Error(`Sender does not match DID in message with kid ${header?.kid}`);
}
const didDocument: DIDDocument = await this.resolveDidDoc(message.from);
let vms = resolveVerificationMethods(didDocument);
if (!vms?.length) {
throw new Error(`No verification methods defined in the DID document of ${didDocument.id}`);
}
if (header.kid) {
const vm = vms.find((v) => {
return v.id === header.kid;
});
if (!vm) {
throw new Error(
`verification method with specified kid ${header.kid} is not found in the DID Document`
);
}
vms = [vm];
}
const verificationResponse = verifyJWS(jws, vms);
if (!verificationResponse) {
throw new Error('JWS verification failed');
}
return message as BasicMessage;
}
mediaType(): MediaType {
return MediaType.SignedMessage;
}
/** {@inheritDoc IPacker.getSupportedProfiles} */
getSupportedProfiles(): string[] {
return this.supportedProtocolVersions.map(
(v) => `${v};env=${this.mediaType()};alg=${this.supportedAlgorithms.join(',')}`
);
}
/** {@inheritDoc IPacker.isProfileSupported} */
isProfileSupported(profile: string) {
const { protocolVersion, env, circuits, alg } = parseAcceptProfile(profile);
if (!this.supportedProtocolVersions.includes(protocolVersion)) {
return false;
}
if (env !== this.mediaType()) {
return false;
}
if (circuits) {
throw new Error(`Circuits are not supported for ${env} media type`);
}
const algSupported =
!alg?.length || alg.some((a) => this.supportedAlgorithms.includes(a as AcceptJwsAlgorithms));
return algSupported;
}
private async resolveDidDoc(from: string) {
let didDocument: DIDDocument;
try {
const didResolutionResult = await this._documentResolver.resolve(from);
if (!didResolutionResult?.didDocument?.id) {
throw new Error(`did document for ${from} is not found in resolution result`);
}
didDocument = didResolutionResult.didDocument;
} catch (err: unknown) {
throw new Error(`did document for ${from} is not resolved: ${(err as Error).message}`);
}
return didDocument;
}
private async packInternal(message: BasicMessage, params: JWSPackerParams): Promise<Uint8Array> {
if (!params.alg) {
throw new Error('Missing algorithm');
}
const from = message.from ?? '';
if (!from) {
throw new Error('Missing sender DID');
}
const vmTypes: string[] =
SUPPORTED_PUBLIC_KEY_TYPES[params.alg as keyof typeof SUPPORTED_PUBLIC_KEY_TYPES];
if (!vmTypes?.length) {
throw new Error(`No supported verification methods for algorithm ${params.alg}`);
}
const didDocument: DIDDocument = params.didDocument ?? (await this.resolveDidDoc(from));
const vms = resolveVerificationMethods(didDocument);
if (!vms.length) {
throw new Error(`No verification methods defined in the DID document of ${didDocument.id}`);
}
// try to find a managed signing key that matches keyRef
const vm = params.kid ? vms.find((vm) => vm.id === params.kid) : vms[0];
if (!vm) {
throw new Error(`No key found with id ${params.kid} in DID document of ${didDocument.id}`);
}
const { publicKeyBytes, kmsKeyType } = extractPublicKeyBytes(vm);
if (!publicKeyBytes && !kmsKeyType) {
if ((vm.blockchainAccountId || vm.ethereumAddress) && !params.signer) {
throw new Error(`No signer provided for ${vm.blockchainAccountId || vm.ethereumAddress}`);
}
}
const kid = vm.id;
const headerObj = { alg: params.alg, kid, typ: MediaType.SignedMessage };
const header = encodeBase64url(JSON.stringify(headerObj));
const msg = encodeBase64url(JSON.stringify(message));
const signingInput = `${header}.${msg}`;
const signingInputBytes = byteEncoder.encode(signingInput);
let signatureBase64: string;
if (params.signer) {
const signature = await params.signer(vm, signingInputBytes);
signatureBase64 = bytesToBase64url(signature);
} else {
if (!publicKeyBytes) {
throw new Error('No public key found');
}
if (!kmsKeyType) {
throw new Error('No KMS key type found');
}
const signatureBytes = await this._kms.sign(
{ type: kmsKeyType, id: keyPath(kmsKeyType, bytesToHex(publicKeyBytes)) },
signingInputBytes,
{ alg: params.alg }
);
signatureBase64 = bytesToBase64url(signatureBytes);
}
return byteEncoder.encode(`${signingInput}.${signatureBase64}`);
}
}