Skip to content

Commit

Permalink
Sign permit response
Browse files Browse the repository at this point in the history
  • Loading branch information
wcalderipe committed Mar 20, 2024
1 parent 870918f commit 2765700
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,14 @@ import {
import { SigningAlg, buildSignerEip191, hash, privateKeyToJwk, signJwt } from '@narval/signature'
import { ConfigModule, ConfigService, Path, PathValue } from '@nestjs/config'
import { Test, TestingModule } from '@nestjs/testing'
import { resolve } from 'path'
import { Config, load } from '../../../../policy-engine.config'
import { OpenPolicyAgentException } from '../../exception/open-policy-agent.exception'
import { OpenPolicyAgentEngine } from '../../open-policy-agent.engine'
import { Result } from '../../type/open-policy-agent.type'

const ONE_ETH = toHex(BigInt('1000000000000000000'))

const REGO_CORE_PATH = resolve(__dirname, '../../rego')
const UNSAFE_ENGINE_PRIVATE_KEY = '0x7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b0847229c47951fca5'

const getJwt = (option: { privateKey: Hex; request: Request; sub: string }): Promise<JwtString> => {
const jwk = privateKeyToJwk(option.privateKey)
Expand Down Expand Up @@ -54,12 +53,18 @@ describe('OpenPolicyAgentEngine', () => {
let engine: OpenPolicyAgentEngine

beforeEach(async () => {
engine = await OpenPolicyAgentEngine.empty(await getConfig('resourcePath')).load()
engine = await OpenPolicyAgentEngine.empty({
resourcePath: await getConfig('resourcePath'),
privateKey: UNSAFE_ENGINE_PRIVATE_KEY
}).load()
})

describe('empty', () => {
it('starts with an empty state', async () => {
const e = OpenPolicyAgentEngine.empty(await getConfig('resourcePath'))
const e = OpenPolicyAgentEngine.empty({
resourcePath: await getConfig('resourcePath'),
privateKey: UNSAFE_ENGINE_PRIVATE_KEY
})

expect(e.getPolicies()).toEqual([])
expect(e.getEntities()).toEqual({
Expand Down Expand Up @@ -120,10 +125,6 @@ describe('OpenPolicyAgentEngine', () => {
{
criterion: Criterion.CHECK_ACTION,
args: [Action.SIGN_TRANSACTION]
},
{
criterion: Criterion.CHECK_PRINCIPAL_ID,
args: [FIXTURE.USER.Alice.id]
}
]
}
Expand All @@ -132,6 +133,7 @@ describe('OpenPolicyAgentEngine', () => {
const e = await new OpenPolicyAgentEngine({
policies,
entities: FIXTURE.ENTITIES,
privateKey: UNSAFE_ENGINE_PRIVATE_KEY,
resourcePath: await getConfig('resourcePath')
}).load()

Expand Down Expand Up @@ -163,6 +165,58 @@ describe('OpenPolicyAgentEngine', () => {
request: evaluation.request
})
})

it('adds access token on permit responses', async () => {
const policies: Policy[] = [
{
then: Then.PERMIT,
name: 'test-policy',
when: [
{
criterion: Criterion.CHECK_ACTION,
args: [Action.SIGN_TRANSACTION]
}
]
}
]

const e = await new OpenPolicyAgentEngine({
policies,
entities: FIXTURE.ENTITIES,
privateKey: UNSAFE_ENGINE_PRIVATE_KEY,
resourcePath: await getConfig('resourcePath')
}).load()

const request = {
action: Action.SIGN_TRANSACTION,
nonce: 'test-nonce',
transactionRequest: {
from: FIXTURE.WALLET.Engineering.address,
to: FIXTURE.WALLET.Testing.address,
value: ONE_ETH,
chainId: 1
},
resourceId: FIXTURE.WALLET.Engineering.id
}

const evaluation: EvaluationRequest = {
authentication: await getJwt({
privateKey: FIXTURE.UNSAFE_PRIVATE_KEY.Alice,
sub: FIXTURE.USER.Alice.id,
request
}),
request
}

const response = await e.evaluate(evaluation)

expect(response.decision).toEqual(Decision.PERMIT)
// TODO: (@wcalderipe, 20/03/24) Today we can't assert the signature
// because the function isn't pure due to the dependency on Date.now
expect(response.accessToken).toMatchObject({
value: expect.any(String)
})
})
})

describe('decide', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,23 @@ import {
Entities,
EvaluationRequest,
EvaluationResponse,
JsonWebKey,
JwtString,
Policy
} from '@narval/policy-engine-shared'
import { Hex, decode, hash, publicKeyToJwk, verifyJwt } from '@narval/signature'
import {
Hex,
Payload,
SigningAlg,
base64UrlToHex,
buildSignerEip191,
decode,
hash,
privateKeyToJwk,
publicKeyToJwk,
signJwt,
verifyJwt
} from '@narval/signature'
import { HttpStatus } from '@nestjs/common'
import { loadPolicy } from '@open-policy-agent/opa-wasm'
import { compact } from 'lodash/fp'
Expand All @@ -31,15 +44,22 @@ export class OpenPolicyAgentEngine implements Engine<OpenPolicyAgentEngine> {

private resourcePath: string

// TODO: (@wcalderipe, 20/03/24) How we store and recover a signing key will
// change very soon because we need to support MPC signing.
// This code is here just for feature parity with the existing proof of
// concept.
private privateKey: Hex

private opa?: OpenPolicyAgentInstance

constructor(params: { policies: Policy[]; entities: Entities; resourcePath: string }) {
constructor(params: { policies: Policy[]; entities: Entities; resourcePath: string; privateKey: Hex }) {
this.entities = params.entities
this.policies = params.policies
this.privateKey = params.privateKey
this.resourcePath = params.resourcePath
}

static empty(resourcePath: string): OpenPolicyAgentEngine {
static empty(params: { resourcePath: string; privateKey: Hex }): OpenPolicyAgentEngine {
return new OpenPolicyAgentEngine({
entities: {
addressBook: [],
Expand All @@ -54,7 +74,8 @@ export class OpenPolicyAgentEngine implements Engine<OpenPolicyAgentEngine> {
wallets: []
},
policies: [],
resourcePath: resourcePath
resourcePath: params.resourcePath,
privateKey: params.privateKey
})
}

Expand Down Expand Up @@ -120,13 +141,11 @@ export class OpenPolicyAgentEngine implements Engine<OpenPolicyAgentEngine> {
})
}

const signedMessage = hash(evaluation.request)
const principalCredential = await this.verifySignature(evaluation.authentication, signedMessage)
const message = hash(evaluation.request)
const principalCredential = await this.verifySignature(evaluation.authentication, message)

const approvalsCredential = await Promise.all(
(evaluation.approvals ? evaluation.approvals : []).map((signature) =>
this.verifySignature(signature, signedMessage)
)
(evaluation.approvals ? evaluation.approvals : []).map((signature) => this.verifySignature(signature, message))
)

const results = await this.opaEvaluate(evaluation, {
Expand All @@ -135,11 +154,25 @@ export class OpenPolicyAgentEngine implements Engine<OpenPolicyAgentEngine> {
})
const decision = this.decide(results)

return {
const response: EvaluationResponse = {
decision: decision.decision,
approvals: decision.approvals,
request: evaluation.request
}

if (response.decision === Decision.PERMIT) {
return {
...response,
accessToken: {
value: await this.sign({
principalCredential,
message
})
}
}
}

return response
}

private async verifySignature(signature: JwtString, message: string) {
Expand Down Expand Up @@ -269,4 +302,32 @@ export class OpenPolicyAgentEngine implements Engine<OpenPolicyAgentEngine> {
private isPermitMissingApproval(result: Result): boolean {
return Boolean(result.reasons?.some((reason) => reason.type === 'permit' && reason.approvalsMissing.length > 0))
}

private async sign(params: { principalCredential: CredentialEntity; message: string }): Promise<JwtString> {
const engineJwk: JsonWebKey = privateKeyToJwk(this.privateKey)
const principalJwk = publicKeyToJwk(params.principalCredential.pubKey as Hex)

const payload: Payload = {
requestHash: params.message,
sub: params.principalCredential.userId,
// TODO: iat & exp values must be arguments, cannot generate timestamps
// because of cluster mis-match
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 60 * 10, // 10 minutes
// TODO: allow client-specific; should come from config
iss: 'https://armory.narval.xyz',
// aud: TODO
// jti: TODO
cnf: principalJwk
}

if (!engineJwk.d) {
throw new OpenPolicyAgentException({
message: 'Missing signing private key',
suggestedHttpStatusCode: HttpStatus.INTERNAL_SERVER_ERROR
})
}

return signJwt(payload, engineJwk, { alg: SigningAlg.EIP191 }, buildSignerEip191(base64UrlToHex(engineJwk.d)))
}
}

0 comments on commit 2765700

Please sign in to comment.