diff --git a/.github/workflows/lint_test_build.yml b/.github/workflows/lint_test_build.yml index 6faaf343e2..cdf00b2a0a 100644 --- a/.github/workflows/lint_test_build.yml +++ b/.github/workflows/lint_test_build.yml @@ -64,6 +64,15 @@ jobs: - uses: ./.github/workflows/rafiki/env-setup - run: pnpm --filter openapi test + mock-account-provider: + runs-on: ubuntu-latest + needs: checkout + timeout-minutes: 5 + steps: + - uses: actions/checkout@v2 + - uses: ./.github/workflows/rafiki/env-setup + - run: pnpm --filter mock-account-provider build + open-payments: runs-on: ubuntu-latest needs: checkout @@ -82,6 +91,7 @@ jobs: - frontend - auth - openapi + - mock-account-provider - open-payments steps: - uses: actions/checkout@v2 diff --git a/packages/auth/seeds/development/01_grants.js b/packages/auth/seeds/development/01_grants.js index e783b0243a..8c9e4ed304 100644 --- a/packages/auth/seeds/development/01_grants.js +++ b/packages/auth/seeds/development/01_grants.js @@ -11,8 +11,9 @@ exports.seed = function (knex) { startMethod: ['redirect'], finishMethod: 'redirect', finishUri: 'https://example.com/finish', + client: 'https://backend/accounts/gfranklin', + clientKeyId: 'keyid-742ab7cd-1624-4d2e-af6e-e15a71638669', clientNonce: 'example-client-nonce', - clientKeyId: 'http://fynbos/keys/1234', continueToken: '566a929a-86bb-41b8-b12d-718fa4ab2db2', continueId: '92c98ab7-9240-43b4-a86f-402f1c6fd6f5', interactId: 'example-interact-id', @@ -27,8 +28,9 @@ exports.seed = function (knex) { startMethod: ['redirect'], finishMethod: 'redirect', finishUri: 'http://peer-auth:3006/finish', + client: 'https://peer-backend/accounts/pfry', + clientKeyId: 'keyid-97a3a431-8ee1-48fc-ac85-70e2f5eba8e5', clientNonce: 'example-client-nonce', - clientKeyId: 'http://local-bank/keys/1234', continueToken: 'fc7d255b-66f7-46f5-af56-65831a110604', continueId: '006856cd-a34a-4d4a-bb69-af1e07980834', interactId: 'local-bank-interact-id', @@ -43,8 +45,9 @@ exports.seed = function (knex) { startMethod: ['redirect'], finishMethod: 'redirect', finishUri: 'http://localhost:3300/mock-idp/fake-client?', + client: 'https://backend/accounts/gfranklin', + clientKeyId: 'keyid-742ab7cd-1624-4d2e-af6e-e15a71638669', clientNonce: 'demo-client-nonce', - clientKeyId: 'http://fynbos/keys/1234', continueToken: '301294e4-db8d-445b-b77e-d27583719ecc', continueId: 'edbe6928-7d80-44a8-94b8-514e75759439', interactId: 'demo-interact-id', diff --git a/packages/mock-account-provider/app/lib/crypto.server.ts b/packages/mock-account-provider/app/lib/crypto.server.ts new file mode 100644 index 0000000000..3c9d7c8cbc --- /dev/null +++ b/packages/mock-account-provider/app/lib/crypto.server.ts @@ -0,0 +1,22 @@ +import { createPublicKey, generateKeyPairSync, type JsonWebKey } from 'crypto' + +type OpenPaymentsJWK = JsonWebKey & { + alg: 'EdDSA' + kid: string +} + +export const generateJwk = ({ keyId }: { keyId: string }): OpenPaymentsJWK => { + const jwk = createPublicKey(generateKeyPairSync('ed25519').privateKey).export( + { + format: 'jwk' + } + ) + + return { + alg: 'EdDSA', + kid: keyId, + kty: jwk.kty, + crv: jwk.crv, + x: jwk.x + } +} diff --git a/packages/mock-account-provider/app/lib/requesters.ts b/packages/mock-account-provider/app/lib/requesters.ts index 738669afb1..efe84e36ec 100644 --- a/packages/mock-account-provider/app/lib/requesters.ts +++ b/packages/mock-account-provider/app/lib/requesters.ts @@ -2,8 +2,10 @@ import { gql } from '@apollo/client' import type { CreatePeerMutationResponse, LiquidityMutationResponse, - CreatePaymentPointerMutationResponse, - PaymentPointer + PaymentPointer, + CreatePaymentPointerKeyMutationResponse, + CreatePaymentPointerKeyInput, + CreatePaymentPointerInput } from '../../generated/graphql' import { apolloClient } from './apolloClient' @@ -113,7 +115,7 @@ export async function createPaymentPointer( accountUrl: string, assetCode: string, assetScale: number -): Promise { +): Promise { const createPaymentPointerMutation = gql` mutation CreatePaymentPointer($input: CreatePaymentPointerInput!) { createPaymentPointer(input: $input) { @@ -128,27 +130,70 @@ export async function createPaymentPointer( } } ` - const createPaymentPointerInput = { - input: { - asset: { - code: assetCode, - scale: assetScale - }, - url: accountUrl, - publicName: accountName - } + const createPaymentPointerInput: CreatePaymentPointerInput = { + asset: { + code: assetCode, + scale: assetScale + }, + url: accountUrl, + publicName: accountName } + return apolloClient .mutate({ mutation: createPaymentPointerMutation, - variables: createPaymentPointerInput + variables: { + input: createPaymentPointerInput + } + }) + .then(({ data }) => { + console.log(data) + + if ( + !data.createPaymentPointer.success || + !data.createPaymentPointer.paymentPointer + ) { + throw new Error('Data was empty') + } + + return data.createPaymentPointer.paymentPointer + }) +} + +export async function createPaymentPointerKey({ + paymentPointerId, + jwk +}: { + paymentPointerId: string + jwk: string +}): Promise { + const createPaymentPointerKeyMutation = gql` + mutation CreatePaymentPointerKey($input: CreatePaymentPointerKeyInput!) { + createPaymentPointerKey(input: $input) { + code + success + message + } + } + ` + const createPaymentPointerKeyInput: CreatePaymentPointerKeyInput = { + paymentPointerId, + jwk + } + + return apolloClient + .mutate({ + mutation: createPaymentPointerKeyMutation, + variables: { + input: createPaymentPointerKeyInput + } }) - .then(({ data }): CreatePaymentPointerMutationResponse => { + .then(({ data }): CreatePaymentPointerKeyMutationResponse => { console.log(data) - if (!data.createPaymentPointer.success) { + if (!data.createPaymentPointerKey.success) { throw new Error('Data was empty') } - return data.createPaymentPointer + return data.createPaymentPointerKey }) } diff --git a/packages/mock-account-provider/app/lib/run_seed.ts b/packages/mock-account-provider/app/lib/run_seed.ts index 341b648e04..03d9d6a30c 100644 --- a/packages/mock-account-provider/app/lib/run_seed.ts +++ b/packages/mock-account-provider/app/lib/run_seed.ts @@ -4,10 +4,12 @@ import type { SeedInstance, Account, Peering } from './parse_config' import { createPeer, addPeerLiquidity, - createPaymentPointer + createPaymentPointer, + createPaymentPointerKey } from './requesters' import { v4 } from 'uuid' import { mockAccounts } from './accounts.server' +import { generateJwk } from './crypto.server' export async function setupFromSeed(config: SeedInstance): Promise { const peerResponses = await Promise.all( @@ -52,7 +54,7 @@ export async function setupFromSeed(config: SeedInstance): Promise { false ) } - const pp = await createPaymentPointer( + const paymentPointer = await createPaymentPointer( config.self.graphqlUrl, account.name, `https://${CONFIG.self.hostname}/${account.path}`, @@ -62,11 +64,16 @@ export async function setupFromSeed(config: SeedInstance): Promise { await mockAccounts.setPaymentPointer( account.id, - pp.paymentPointer?.id, - pp.paymentPointer?.url + paymentPointer.id, + paymentPointer.url ) - return pp + await createPaymentPointerKey({ + paymentPointerId: paymentPointer.id, + jwk: JSON.stringify(generateJwk({ keyId: `keyid-${account.id}` })) + }) + + return paymentPointer }) ) console.log(JSON.stringify(accountResponses, null, 2)) diff --git a/packages/open-payments/src/index.ts b/packages/open-payments/src/index.ts index 78b8b06253..b9ff461d0f 100644 --- a/packages/open-payments/src/index.ts +++ b/packages/open-payments/src/index.ts @@ -16,3 +16,5 @@ export { AuthenticatedClient, UnauthenticatedClient } from './client' + +export { generateJwk } from './jwk' diff --git a/packages/open-payments/src/jwk.test.ts b/packages/open-payments/src/jwk.test.ts new file mode 100644 index 0000000000..5dc94b08de --- /dev/null +++ b/packages/open-payments/src/jwk.test.ts @@ -0,0 +1,44 @@ +import { generateKeyPairSync } from 'crypto' +import { generateJwk } from './jwk' + +describe('jwk', (): void => { + describe('generateJwk', (): void => { + test('properly generates jwk', async (): Promise => { + expect(generateJwk({ keyId: 'keyid' })).toEqual({ + alg: 'EdDSA', + kid: 'keyid', + kty: 'OKP', + crv: 'Ed25519', + x: expect.any(String) + }) + }) + + test('properly generates jwk with defined private key', async (): Promise => { + expect( + generateJwk({ + keyId: 'keyid', + privateKey: generateKeyPairSync('ed25519').privateKey + }) + ).toEqual({ + alg: 'EdDSA', + kid: 'keyid', + kty: 'OKP', + crv: 'Ed25519', + x: expect.any(String) + }) + }) + + test('throws if empty keyId', async (): Promise => { + expect(() => generateJwk({ keyId: '' })).toThrow('KeyId cannot be empty') + }) + + test('throws if provided key is not EdDSA-Ed25519', async (): Promise => { + expect(() => + generateJwk({ + keyId: 'keyid', + privateKey: generateKeyPairSync('ed448').privateKey + }) + ).toThrow('Key is not EdDSA-Ed25519') + }) + }) +}) diff --git a/packages/open-payments/src/jwk.ts b/packages/open-payments/src/jwk.ts new file mode 100644 index 0000000000..19fc51e954 --- /dev/null +++ b/packages/open-payments/src/jwk.ts @@ -0,0 +1,34 @@ +import { createPublicKey, generateKeyPairSync, KeyObject } from 'crypto' +import { JWK } from './types' + +export const generateJwk = ({ + privateKey: providedPrivateKey, + keyId +}: { + privateKey?: KeyObject + keyId: string +}): JWK => { + if (!keyId.trim()) { + throw new Error('KeyId cannot be empty') + } + + const privateKey = providedPrivateKey + ? providedPrivateKey + : generateKeyPairSync('ed25519').privateKey + + const jwk = createPublicKey(privateKey).export({ + format: 'jwk' + }) + + if (jwk.crv !== 'Ed25519' || jwk.kty !== 'OKP') { + throw new Error('Key is not EdDSA-Ed25519') + } + + return { + alg: 'EdDSA', + kid: keyId, + kty: jwk.kty, + crv: jwk.crv, + x: jwk.x + } +}