@ki1r0y/signed-cloud-server
Version:
Basic cloud storage in which contents are cryptographically signed, using distributed-storage as a co-dependency.
123 lines (119 loc) • 6.29 kB
JavaScript
// The distributed-security package has unit tests that sets up the package to use
// a simple in-memory storage that is independent of the cloud storage implementation.
// Here, however, we exercise distributed-security and signed-storage as co-dependencies
// of each other:
// - These tests use the default behavior of distributed-security.
// - The default behavior of distributed-security is to use signed-cloud-client.
// - The default behavior of signed-cloud-server is to use distributed-security
import Security from "@ki1r0y/distributed-security";
const Storage = Security.Storage; // Just shorthand.
async function checkSignedResult(collectionName, tag) {
// Retrieve specific resource and make sure it signed appropriately.
let body = await Storage.retrieve(collectionName, tag),
verified = await Security.verify(body);
expect(verified).toBeTruthy();
expect(verified.protectedHeader.kid || verified.protectedHeader.iss).toBeTruthy();
}
const noCache = {headers: { // Options to make the browser's fetch not cache. (You, too, Safari!)
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
'Expires': '0',
}};
async function checkEmptyResult(collectionName, tag) {
// Confirm that retrieval of a specific resource is empty.
expect(await Storage.retrieve(collectionName, tag, noCache)).toBeFalsy();
}
describe("Signed Cloud", function () {
let member1, member2, team;
beforeAll(async function () {
member1 = await Security.create();
member2 = await Security.create();
team = await Security.create(member1);
await checkSignedResult('EncryptionKey', member1);
await checkSignedResult('EncryptionKey', member2);
await checkSignedResult('EncryptionKey', team);
await checkSignedResult('Team', team);
}, 10e3);
it('verifies after change of membership, but tracks membership.', async function () {
let teamSig = await Storage.retrieve('Team', team),
verified = await Security.verify(teamSig, {team, member: member1, notBefore: 'Team'});
expect(verified).toBeTruthy();
await Security.changeMembership({tag: team, add: [member2], remove: [member1]});
teamSig = await Storage.retrieve('Team', team, noCache);
verified = await Security.verify(teamSig, {team, member: false}); // Do not check that signer is a current member.
expect(verified).toBeTruthy();
expect(verified.protectedHeader.act).toBe(member1); // not member2, who is the current member
expect(await Security.verify(teamSig, {team})).toBeUndefined(); // because not signed by a current member
});
it('stores recovery tags.', async function () {
let recoveryTag = await Security.create({prompt: 'Test password:'});
await checkSignedResult('EncryptionKey', recoveryTag);
await checkSignedResult('KeyRecovery', recoveryTag);
await Security.destroy(recoveryTag);
await checkEmptyResult('EncryptionKey', recoveryTag);
await checkEmptyResult('KeyRecovery', recoveryTag);
}, 10e3);
it('defines origin.', function () {
expect(Storage.origin).toBeTruthy();
});
it('defines uri.', function () {
expect(Storage.uri).toBeTruthy();
});
it('read answers json with proper mime type.', async function () {
let response = await fetch(Storage.uri('EncryptionKey', member1));
expect(response.ok).toBeTruthy();
expect(response.headers.get('Content-Type')).toContain('application/json');
});
describe('write', function () {
let anotherTeam, url, signatureByRemovedMember, signatureByFinalMember, verified;
beforeAll(async function () {
anotherTeam = await Security.create(member1, member2);
url = Storage.uri('Team', anotherTeam);
signatureByRemovedMember = await fetch(url).then(response => response.json());
await Security.changeMembership({tag: anotherTeam, remove: [member1]});
let ending = await fetch(url).then(response => response.json());
verified = await Security.verify(ending, {team: anotherTeam, member: false}); // Won't deep verify because we removed that member.
signatureByFinalMember = await Security.sign(verified.json, {team: anotherTeam, time: Date.now()});
});
it('resaves.', async function () {
let response = await fetch(url, {method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(signatureByFinalMember)});
expect(response.ok).toBeTruthy();
});
it('resaves a correctly resigned payload.', async function () {
let response = await fetch(url, {method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(signatureByFinalMember)});
expect(response.ok).toBeTruthy();
});
it('write rejects non-json writes.', async function () {
let response = await fetch(url, {method: 'PUT', headers: {'Content-Type': 'application/text'}, body: JSON.stringify(signatureByFinalMember)});
expect(response.ok).toBeFalsy();
expect(response.status).toBe(415); // Unsupported Media Type
});
it('rejects storage with insufficient signature.', async function () {
let resigned = await Security.sign(verified.json, member2), // right member, but not sufficiently auditable.
response = await fetch(url, {method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(resigned)});
expect(response.ok).toBeFalsy();
expect(response.status).toBe(403); // Forbidden
});
it('rejects storage by non-member.', async function () {
let response = await fetch(url, {method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(signatureByRemovedMember)});
expect(response.ok).toBeFalsy();
expect(response.status).toBe(403); // Forbidden
});
afterAll(async function () {
await Security.destroy(anotherTeam);
await checkEmptyResult('Team', anotherTeam);
});
});
it('get provides headers for caching.', async function () {
// TODO!
});
afterAll(async function () {
await Security.destroy(team);
await Security.destroy(member1);
await Security.destroy(member2);
await checkEmptyResult('EncryptionKey', member1);
await checkEmptyResult('EncryptionKey', member2);
await checkEmptyResult('EncryptionKey', team);
await checkEmptyResult('Team', team);
});
});