diff --git a/package-lock.json b/package-lock.json index 154ffd08..1d01e1dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,12 @@ "dependencies": { "@oasisprotocol/sapphire-paratime": "^1.3.2", "@oceanprotocol/contracts": "^2.2.0", + "axios": "^1.7.7", "cross-fetch": "^4.0.0", "crypto-js": "^4.1.1", "decimal.js": "^10.4.1", - "ethers": "^5.7.2" + "ethers": "^5.7.2", + "jose": "^5.9.6" }, "devDependencies": { "@truffle/hdwallet-provider": "^2.0.14", @@ -4499,6 +4501,17 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-plugin-dynamic-import-node": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", @@ -8667,6 +8680,26 @@ "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -8696,6 +8729,20 @@ "node": "*" } }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/form-data-encoder": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.1.tgz", @@ -10798,6 +10845,15 @@ "node": ">=8" } }, + "node_modules/jose": { + "version": "5.9.6", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.9.6.tgz", + "integrity": "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-sha3": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", @@ -14037,8 +14093,7 @@ "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "node_modules/prr": { "version": "1.0.1", @@ -21018,6 +21073,16 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" }, + "axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "requires": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "babel-plugin-dynamic-import-node": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", @@ -24274,6 +24339,11 @@ "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", "dev": true }, + "follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==" + }, "for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -24297,6 +24367,16 @@ "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, + "form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "form-data-encoder": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.1.tgz", @@ -25811,6 +25891,11 @@ } } }, + "jose": { + "version": "5.9.6", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.9.6.tgz", + "integrity": "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==" + }, "js-sha3": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", @@ -28230,8 +28315,7 @@ "proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "prr": { "version": "1.0.1", diff --git a/package.json b/package.json index 283e17ba..85b301ca 100644 --- a/package.json +++ b/package.json @@ -56,10 +56,12 @@ "dependencies": { "@oasisprotocol/sapphire-paratime": "^1.3.2", "@oceanprotocol/contracts": "^2.2.0", + "axios": "^1.7.7", "cross-fetch": "^4.0.0", "crypto-js": "^4.1.1", "decimal.js": "^10.4.1", - "ethers": "^5.7.2" + "ethers": "^5.7.2", + "jose": "^5.9.6" }, "devDependencies": { "@truffle/hdwallet-provider": "^2.0.14", diff --git a/src/@types/IssuerSignature.ts b/src/@types/IssuerSignature.ts new file mode 100644 index 00000000..932100c2 --- /dev/null +++ b/src/@types/IssuerSignature.ts @@ -0,0 +1,18 @@ +export interface JWT { + kty: string + d: string + crv: string + kid: string + x: string +} + +export interface IssuerKey { + type: string + jwk: JWT +} + +export interface SignedCredential { + jws: string + header: Record + issuer: string +} diff --git a/src/utils/SignDDO.ts b/src/utils/SignDDO.ts new file mode 100644 index 00000000..9b857b79 --- /dev/null +++ b/src/utils/SignDDO.ts @@ -0,0 +1,87 @@ +import { base64url, importJWK, JWTPayload, SignJWT } from 'jose' +import axios from 'axios' +import { ethers } from 'ethers' +import { IssuerKey, SignedCredential } from '../@types/IssuerSignature' + +/** + * Signs a verifiable credential using Walt.id's issuer API. + * @param {any} verifiableCredential - The verifiable credential to sign. + * @param {string} waltIdIssuerApi - URL of the Walt.id issuer API. + * @param {string} issuerDid - DID of the issuer. + * @param {IssuerKey} issuerKey - Issuer's key for signing. + * @returns {Promise} - The signed credential's JWS, header, and issuer information. + * @throws {Error} If the signing process fails. + */ +export async function signCredentialWithWaltId( + verifiableCredential: any, + waltIdIssuerApi: string, + issuerDid: string, + issuerKey: IssuerKey +): Promise { + try { + const response = await axios.post(waltIdIssuerApi, { + credentialData: verifiableCredential, + issuerDid, + issuerKey, + subjectDid: verifiableCredential.credentialSubject.id + }) + const jws = response.data + const header = { alg: issuerKey.jwk.kty } + return { jws, header, issuer: issuerDid } + } catch (error) { + console.error('Error signing credential with WaltId:', error) + throw error + } +} + +/** + * Signs a verifiable credential locally using a private key. + * @param {any} verifiableCredential - The verifiable credential to sign. + * @param {string} privateKey - The private key. + * @returns {Promise} - The signed credential's JWS, header, and issuer information. + * @throws {Error} If the signing process fails. + */ +export async function signCredential( + verifiableCredential: any, + privateKey: string +): Promise { + try { + const wallet = new ethers.Wallet(privateKey) + const privateKeyBuffer = Buffer.from(privateKey.substring(2), 'hex') + const publicKeyHex = wallet._signingKey().publicKey + const publicKeyBuffer = Buffer.from(publicKeyHex.substring(2), 'hex') + + // Extract x and y coordinates from the public key buffer + const xBuffer = publicKeyBuffer.slice(1, 33) + const yBuffer = publicKeyBuffer.slice(33, 65) + + // Base64url-encode the values + const d = base64url.encode(privateKeyBuffer as any as Uint8Array) + const x = base64url.encode(xBuffer as any as Uint8Array) + const y = base64url.encode(yBuffer as any as Uint8Array) + + const privateJwk = { + kty: 'EC', + crv: 'secp256k1', + d, + x, + y, + alg: 'ES256K', + use: 'sig' + } + + const key = await importJWK(privateJwk, 'ES256K') + + const jws = await new SignJWT(verifiableCredential as unknown as JWTPayload) + .setProtectedHeader({ alg: 'ES256K' }) + .setIssuedAt() + .setIssuer(publicKeyHex) + .sign(key) + const header = { alg: 'ES256K' } + + return { jws, header, issuer: publicKeyHex } + } catch (error) { + console.error('Error signing credential:', error) + throw error + } +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 55da7cf7..8b6c8ade 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -11,3 +11,4 @@ export * from './TokenUtils' export * from './ProviderErrors' export * from './OrderUtils' export * from './Assets' +export * from './SignDDO'