Skip to content

Commit

Permalink
#10: Supporting user keys for signatures
Browse files Browse the repository at this point in the history
  • Loading branch information
gsvarovsky committed Nov 20, 2023
1 parent a6938dd commit 39e21c2
Show file tree
Hide file tree
Showing 9 changed files with 80 additions and 37 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ name: Node.js CI

on:
push:
branches: [ "main" ]
branches: [ "main", "edge" ]
pull_request:
branches: [ "main" ]
branches: [ "main", "edge" ]

jobs:
build:
Expand Down
16 changes: 10 additions & 6 deletions src/data/UserKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,12 +226,16 @@ export class UserKey implements Key {
*/
toConfig(authKey: AuthKey): UserKeyConfig {
return Object.assign(authKey.toConfig(), {
key: {
type: 'rsa' as 'rsa', // Typescript weirdness
public: this.publicKey.toString('base64'),
private: this.privateKey?.toString('base64')
// revoked assumed false
}
key: this.getRsaKeyConfig()
});
}

getRsaKeyConfig() {
return {
type: 'rsa' as 'rsa', // Typescript weirdness
public: this.publicKey.toString('base64'),
private: this.privateKey?.toString('base64')
// revoked assumed false
};
}
}
3 changes: 1 addition & 2 deletions src/data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export const gatewayContext = {
};

/**
* Obtains absolute IRIs in the Gateway vocabulary
* @returns {string}
* @returns absolute IRIs in the Gateway vocabulary
*/
export const gatewayVocab = (iri: Iri) => `${$vocab}${iri}`;
4 changes: 2 additions & 2 deletions src/http/SubdomainEndPoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ export class SubdomainEndPoint extends EndPoint<ApiEndPoint> {
user: as.object({
'@id': as.string().uri().required(),
key: asRsaKeyConfig
.keys({ '@id': as.string().uri().required() })
.keys({ keyid: as.string().regex(/\w+/).required() })
.custom(json => new UserKey({
keyid: json['@id'], ...keyPairFromConfig(json)
keyid: json.keyid, ...keyPairFromConfig(json)
})).optional()
}).optional()
}));
Expand Down
2 changes: 1 addition & 1 deletion src/lib/BaseGateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export class BaseGateway {
) {}

ownedRefAsId(tsRef: Reference) {
// A timesheet reference may be relative to the domain base
// A subdomain reference may be relative to the domain base
return AccountOwnedId.fromReference(tsRef, this.domainName);
}

Expand Down
3 changes: 1 addition & 2 deletions src/lib/CloneFactory.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ClassicLevel } from 'classic-level';
import {
AppPrincipal, Attribution, clone as meldClone, ConstructRemotes, InitialApp, MeldClone,
MeldConfig, MeldReadState, MeldTransportSecurity, propertyValue
MeldReadState, MeldTransportSecurity, propertyValue
} from '@m-ld/m-ld';
import { AuthKey } from './AuthKey.js';
import { gatewayVocab } from '../data/index.js';
Expand Down Expand Up @@ -45,7 +45,6 @@ export abstract class CloneFactory {
abstract initialise(address: string): void | Promise<unknown>;

/**
* @param {MeldConfig} config
* @returns {ConstructRemotes | Promise<ConstructRemotes>}
*/
abstract remotes(config: BaseGatewayConfig): ConstructRemotes | Promise<ConstructRemotes>;
Expand Down
12 changes: 7 additions & 5 deletions src/server/Gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,13 @@ export class Gateway extends BaseGateway implements AccountContext {
);
}

// TODO: implement this in timeld
// override this in timeld
onUpdateSubdomain(
_id: AccountOwnedId,
_update: MeldUpdate,
id: AccountOwnedId,
update: MeldUpdate,
_state: MeldReadState
): Promise<void> {
LOG.info(id.toRelativeIri(), JSON.stringify(update));
return Promise.resolve();
}

Expand All @@ -95,6 +96,7 @@ export class Gateway extends BaseGateway implements AccountContext {
key: UserKey
) {
await sd.write({
'@context': gatewayContext,
'@id': this.absoluteId(iri),
'@type': type,
key: key.toJSON(true)
Expand Down Expand Up @@ -235,14 +237,14 @@ export class Gateway extends BaseGateway implements AccountContext {
state = await state.write({
'@id': id.account, subdomain: sdClone.toJSON()
});
if (sdClone.useSignatures && who != null)
await this.writeUserToSubdomain(state, sdClone, who);
this.subdomainCache.set(id.toDomain(), sdClone);
} else if (src != null &&
spec.useSignatures != null &&
Subdomain.fromJSON(src).useSignatures !== spec.useSignatures) {
throw new ConflictError('Cannot change use of signatures after creation');
}
if (sdClone.useSignatures && who != null)
await this.writeUserToSubdomain(state, sdClone, who);
});
return this.getSubdomainConfig(id, false, who);
}
Expand Down
4 changes: 3 additions & 1 deletion test/Gateway.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,9 @@ describe('Gateway', () => {
// Expect subdomain clone to exist and contain user principal for signing
const sdc = (await gateway.getSubdomain(gateway.ownedId(sd)))!;
await expect(sdc.state.get('http://ex.org/test')).resolves.toEqual({
'@id': 'http://ex.org/test', '@type': 'Account', key: { '@id': '.keyid' }
'@id': 'http://ex.org/test',
'@type': 'http://gw.m-ld.org/#Account',
'http://gw.m-ld.org/#key': { '@id': '.keyid' }
});
});

Expand Down
69 changes: 53 additions & 16 deletions test/http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,26 +251,63 @@ describe('Gateway HTTP API', () => {
});
});

test('puts new subdomain with JWT', async () => {
await acc.update({ '@insert': { remotesAuth: 'jwt' } });
cloneFactory.reusableConfig.mockImplementation(async (_config, context) => ({
jwt: await context?.mintJwt?.()
}));
const res = await request(app)
.put('/api/v1/domain/test/sd1')
.auth('test', 'app.keyid:secret')
.accept('application/json')
.send({ user: { '@id': 'http://ex.org/fred' } });
expect(res.status).toBe(200);
expect(res.body).toMatchObject({
'@domain': 'sd1.test.ex.org', genesis: false, jwt: anyString()
describe('puts new subdomain with JWT', () => {
beforeEach(async () => {
await acc.update({ '@insert': { remotesAuth: 'jwt' } });
cloneFactory.reusableConfig.mockImplementation(async (_config, context) => ({
jwt: await context?.mintJwt?.()
}));
});
expect(decode(res.body.jwt, { json: true })).toMatchObject({
iss: 'http://ex.org/test', sub: 'http://ex.org/fred'

test('without user key', async () => {
const res = await request(app)
.put('/api/v1/domain/test/sd1')
.auth('test', 'app.keyid:secret')
.accept('application/json')
.send({ user: { '@id': 'http://ex.org/fred' } });
expect(res.status).toBe(200);
expect(res.body).toMatchObject({
'@domain': 'sd1.test.ex.org', genesis: false, jwt: anyString()
});
expect(decode(res.body.jwt, { json: true })).toMatchObject({
iss: 'http://ex.org/test', sub: 'http://ex.org/fred'
});
});

test('with user key', async () => {
const userKey = UserKey.generate('app.keyid:secret');
const rsaKeyConfig = userKey.getRsaKeyConfig();
const res = await request(app)
.put('/api/v1/domain/test/sd1')
.auth('test', 'app.keyid:secret')
.accept('application/json')
.send({
useSignatures: true,
user: {
'@id': 'http://ex.org/fred',
key: { keyid: 'keyid', public: rsaKeyConfig.public }
}
});
expect(res.status).toBe(200);
expect(res.body).toMatchObject({
'@domain': 'sd1.test.ex.org', genesis: false, jwt: anyString()
});
const sdc = await gateway.getSubdomain(gateway.ownedId({
account: 'test', name: 'sd1'
}));
expect(sdc?.useSignatures).toBe(true);
// Note that users inserted into subdomains use the Gateway vocabulary
await expect(sdc?.state.get('http://ex.org/fred')).resolves.toMatchObject({
'@id': 'http://ex.org/fred',
'http://gw.m-ld.org/#key': { '@id': '.keyid' }
});
await expect(sdc?.state.get('.keyid')).resolves.toMatchObject({
'http://gw.m-ld.org/#public': expect.any(Buffer)
});
});
});

test.todo('Put subdomain with context');
test.todo('puts subdomain with context');

describe('with subdomain', () => {
let sdId: AccountOwnedId;
Expand Down

0 comments on commit 39e21c2

Please sign in to comment.