UNPKG

@sphereon/gx-compliance-client

Version:

<!--suppress HtmlDeprecatedAttribute --> <h1 align="center"> <br> <a href="https://www.sphereon.com"><img src="https://sphereon.com/content/themes/sphereon/assets/img/logo.svg" alt="Sphereon" width="400"></a> <br>Gaia-X Compliance client (Typescript

290 lines (252 loc) 8.37 kB
// @ts-ignore import jsonld from 'jsonld' import { subtle } from '@transmute/web-crypto-key-pair' import { JsonWebKey } from './JsonWebKeyWithRSASupport' import * as u8a from 'uint8arrays' import { Verifier } from '@transmute/jose-ld' import sec from '@transmute/security-context' /** * WARNING: * * This suite is made specifically to be interoperable with Gaia-X. Do not use this suite for other purposes, as the current Gaia-X implementation contains multiple errors and does not conform to JsonWebSignature2020. * If you do need regular JsonWebSignature2020 support, please configure the SphereonWebSignature2020 class when setting up the agent */ const sha256 = async (data: any) => { return Buffer.from(await subtle.digest('SHA-256', Buffer.from(data))) } export interface JsonWebSignatureOptions { key?: JsonWebKey date?: any verifier?: Verifier } export class JsonWebSignature { public useNativeCanonize: boolean = false public key?: JsonWebKey public proof: any public date: any public type: string = 'JsonWebSignature2020' public verificationMethod?: string public verifier?: Verifier constructor(options: JsonWebSignatureOptions = {}) { this.date = options.date if (options.key) { this.key = options.key this.verificationMethod = this.key.id } if (options.verifier) { this.verifier = options.verifier } } ensureSuiteContext({ document }: any) { const contextUrl = sec.constants.JSON_WEB_SIGNATURE_2020_V1_URL if (document['@context'] === contextUrl || (Array.isArray(document['@context']) && document['@context'].includes(contextUrl))) { // document already includes the required context return } // throw new TypeError(`The document to be signed must contain this suite's @context, ` + `"${contextUrl}".`) } async canonize(input: any, { documentLoader }: any) { return await jsonld.canonize(input, { algorithm: 'URDNA2015', format: 'application/n-quads', documentLoader: documentLoader, }) } async sign({ verifyData, proof }: any) { try { const signer: any = await this.key?.signer() const detachedJws = await signer.sign({ data: verifyData }) proof.jws = detachedJws return proof } catch (e) { console.warn('Failed to sign.') throw e } } async createProof({ document, purpose, documentLoader, expansionMap, compactProof }: any) { let proof const context = document['@context'] if (this.proof) { // use proof JSON-LD document passed to API proof = await jsonld.compact(this.proof, context, { documentLoader, skipExpansion: true, expansionMap, compactToRelative: false, }) } else { // create proof JSON-LD document proof = { '@context': context, } } // ensure proof type is set proof.type = this.type // set default `now` date if not given in `proof` or `options` let date = this.date if (proof.created === undefined && date === undefined) { date = new Date() } // ensure date is in string format if (date !== undefined && typeof date !== 'string') { date = new Date(date).toISOString() date = date.substr(0, date.length - 5) + 'Z' } // add API overrides if (date !== undefined) { proof.created = date } // `verificationMethod` is for newer suites, `creator` for legacy if (this.verificationMethod !== undefined) { proof.verificationMethod = this.verificationMethod } // allow purpose to update the proof; the `proof` is in the // SECURITY_CONTEXT_URL `@context` -- therefore the `purpose` must // ensure any added fields are also represented in that same `@context` proof = await purpose.update(proof, { document, suite: this, documentLoader, expansionMap, }) // create data to sign const verifyData = await this.createVerifyData({ document, proof, documentLoader, expansionMap, compactProof, }) // sign data proof = await this.sign({ verifyData, document, proof, documentLoader, expansionMap, }) delete proof['@context'] return proof } async getVerificationMethod({ proof, documentLoader, instance }: any) { let { verificationMethod } = proof if (!verificationMethod) { // backwards compatibility support for `creator` const { creator } = proof verificationMethod = creator } if (typeof verificationMethod === 'object') { verificationMethod = verificationMethod.id } if (!verificationMethod) { throw new Error('No "verificationMethod" or "creator" found in proof.') } // Note: `expansionMap` is intentionally not passed; we can safely drop // properties here and must allow for it const { document } = await documentLoader(verificationMethod) const framed = await jsonld.frame( verificationMethod, { '@context': document['@context'], '@embed': '@always', id: verificationMethod, }, { // use the cache of the document we just resolved when framing documentLoader: (iri: string) => { if (iri.startsWith(document.id)) { return { documentUrl: iri, document, } } return documentLoader(iri) }, } ) if (!instance) { if (!framed || !framed.controller) { throw new Error(`Verification method ${verificationMethod} not found.`) } return framed } return JsonWebKey.from(document, { signer: false, verifier: this.verifier }) } async verifySignature({ verifyData, verificationMethod, proof }: any) { if (verificationMethod.publicKey) { const key = verificationMethod.publicKey as CryptoKey const signature = proof.jws.split('.')[2] const headerString = proof.jws.split('.')[0] const dataBuffer = u8a.fromString(verifyData, 'utf-8') const messageBuffer = u8a.concat([u8a.fromString(`${headerString}.`, 'utf-8'), dataBuffer]) return await subtle.verify( { name: key.algorithm?.name ? key.algorithm.name : 'RSASSA-PKCS1-V1_5', hash: 'SHA-256', }, key, u8a.fromString(signature, 'base64url'), messageBuffer ) } const verifier = await verificationMethod.verifier() return verifier.verify({ data: verifyData, signature: proof.jws.replace('..', `.${verifyData}.`) }) } async verifyProof({ proof, document, purpose, documentLoader, expansionMap, compactProof }: any) { try { // create data to verify delete document.proof const verifyData = await this.createVerifyData({ document, proof, documentLoader, expansionMap, compactProof, }) // fetch verification method const verificationMethod = await this.getVerificationMethod({ proof, document, documentLoader, expansionMap, instance: true, // this means we get a key pair class instance, not just json. }) // verify signature on data const verified = await this.verifySignature({ verifyData, verificationMethod, // key pair class instance here. document, proof, documentLoader, expansionMap, }) if (!verified) { throw new Error('Invalid signature.') } // ensure proof was performed for a valid purpose const purposeResult = await purpose.validate(proof, { document, suite: this, verificationMethod, documentLoader, expansionMap, }) if (!purposeResult.valid) { throw purposeResult.error } return { verified: true, purposeResult } } catch (error) { return { verified: false, error } } } async createVerifyData({ document, documentLoader }: any) { // concatenate hash of c14n proof options and hash of c14n document const c14nDocument = await this.canonize(document, { documentLoader, }) return u8a.toString(await sha256(c14nDocument), 'base16') } async matchProof({ proof }: any) { return proof.type === 'JsonWebSignature2020' } }