Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,9 @@ PPA_AWARENESS_API_KEY=""
# The messenger platform is discovered on the network by its platformName.
# How to link into it comes from the handles that platform publishes, so there
# is no path to configure here.
# Forge hosting submitted repositories. The signed release statement carries
# owner/name but no host, so without this the repository is shown as plain text.
PPA_REPOSITORY_BASE_URL=""
PPA_MESSENGER_PLATFORM_NAME="meshenger"
# Path that opens a conversation with one person. Only used when the messenger
# publishes no handle for the User ontology; a declared handle always wins.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import { Request, Response } from "express";
import { ProvisioningService, ProvisionRequest, ProvisionResponse } from "../services/ProvisioningService";
import { ProvisioningService, ProvisionRequest, ProvisionResponse, PreviewProvisionRequest, PreviewProvisionResponse } from "../services/ProvisioningService";

export class ProvisioningController {
constructor(private readonly provisioningService: ProvisioningService) {}

registerRoutes(app: any) {
app.post(
"/provision/preview",
async (
req: Request<{}, {}, PreviewProvisionRequest>,
res: Response<PreviewProvisionResponse>,
) => {
const result = await this.provisioningService.previewEVault(req.body);
return res.status(result.success ? 200 : 400).json(result);
},
);
app.post(
"/provision",
async (
Expand Down Expand Up @@ -32,4 +42,3 @@ export class ProvisioningController {
);
}
}

24 changes: 24 additions & 0 deletions infrastructure/evault-core/src/core/http/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import axios from "axios";
import fastify, { type FastifyInstance } from "fastify";
import * as jose from "jose";
import type {
PreviewProvisionRequest,
ProvisionRequest,
ProvisioningService,
} from "../../services/ProvisioningService";
Expand Down Expand Up @@ -1060,6 +1061,29 @@ export async function registerHttpRoutes(

// Provision eVault endpoint
if (provisioningService) {
server.post<{ Body: PreviewProvisionRequest }>(
"/provision/preview",
{
schema: {
tags: ["provisioning"],
description: "Preview an eVault eName without provisioning it",
body: {
type: "object",
required: ["registryEntropy", "namespace"],
properties: {
registryEntropy: { type: "string" },
namespace: { type: "string" },
},
},
},
},
async (request: TypedRequest<PreviewProvisionRequest>, reply: TypedReply) => {
const result = await provisioningService.previewEVault(request.body);
if (!result.success) return reply.status(400).send(result);
return result;
},
);

server.post<{ Body: ProvisionRequest }>(
"/provision",
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ export class GraphQLServer {
"self",
"personal_parameters",
"security_question",
"deployment_key",
"software_version",
] as const;
type ValidType =
(typeof VALID_BINDING_DOCUMENT_TYPES)[number];
Expand Down Expand Up @@ -858,6 +860,8 @@ export class GraphQLServer {
signer: string;
signature: string;
timestamp: string;
scope?: "document" | "bundle";
signedPayload?: string;
};
};
},
Expand All @@ -883,6 +887,8 @@ export class GraphQLServer {
"self",
"personal_parameters",
"security_question",
"deployment_key",
"software_version",
] as const;
type ValidType =
(typeof VALID_BINDING_DOCUMENT_TYPES)[number];
Expand Down Expand Up @@ -996,6 +1002,8 @@ export class GraphQLServer {
signer: string;
signature: string;
timestamp: string;
scope?: "document" | "bundle";
signedPayload?: string;
};
};
},
Expand Down
6 changes: 6 additions & 0 deletions infrastructure/evault-core/src/core/protocol/typedefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,12 +175,16 @@ export const typeDefs = /* GraphQL */ `
self
personal_parameters
security_question
deployment_key
software_version
}

type BindingDocumentSignature {
signer: String!
signature: String!
timestamp: String!
scope: String
signedPayload: String
}

type BindingDocument {
Expand Down Expand Up @@ -300,6 +304,8 @@ export const typeDefs = /* GraphQL */ `
signer: String!
signature: String!
timestamp: String!
scope: String
signedPayload: String
}

"Input for creating a binding document"
Expand Down
29 changes: 27 additions & 2 deletions infrastructure/evault-core/src/core/types/binding-document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,35 @@ export type BindingDocumentType =
| "social_connection"
| "self"
| "personal_parameters"
| "security_question";
| "security_question"
| "deployment_key"
| "software_version";

export interface BindingDocumentSignature {
signer: string;
signature: string;
timestamp: string;
scope?: "document" | "bundle";
signedPayload?: string;
}

export interface BindingDocumentDeploymentKeyData {
kind: "deployment_key";
deploymentName: string;
environment: string;
deployerEname: string;
platformEname: string;
publicKey: string;
algorithm: "ECDSA_P256";
}

export interface BindingDocumentSoftwareVersionData {
kind: "software_version";
platformEname: string;
versionEname: string;
version: string;
releaseTag: string;
commitSha: string;
}

export interface BindingDocumentIdDocumentData {
Expand Down Expand Up @@ -62,7 +85,9 @@ export type BindingDocumentData =
| BindingDocumentSocialConnectionData
| BindingDocumentSelfData
| BindingDocumentPersonalParametersData
| BindingDocumentSecurityQuestionData;
| BindingDocumentSecurityQuestionData
| BindingDocumentDeploymentKeyData
| BindingDocumentSoftwareVersionData;

export interface BindingDocument {
subject: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import {
type StartedNeo4jContainer,
} from "@testcontainers/neo4j";
import neo4j, { type Driver } from "neo4j-driver";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { createHash } from "node:crypto";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { DbService } from "../core/db/db.service";
import { computeEnvelopeHash } from "../core/db/envelope-hash";
import { computeBindingDocumentHash } from "../core/utils/binding-document-hash";
Expand Down Expand Up @@ -233,6 +234,97 @@ describe("BindingDocumentService (integration)", () => {
expect(result.bindingDocument.subject).toBe("@already-prefixed");
});

it("stores a deployer-signed deployment bundle as public documents", async () => {
const deploymentSubject = "@11111111-1111-5111-8111-111111111111";
const versionSubject = "@22222222-2222-5222-8222-222222222222";
const deployer = "@33333333-3333-5333-8333-333333333333";
const deploymentData = {
kind: "deployment_key" as const,
deploymentName: "Singapore production",
environment: "production",
deployerEname: deployer,
platformEname: TEST_ENAME,
publicKey: "zDeploymentPublicKey",
algorithm: "ECDSA_P256" as const,
};
const versionData = {
kind: "software_version" as const,
platformEname: TEST_ENAME,
versionEname: versionSubject,
version: "1.2.3",
releaseTag: "v1.2.3",
commitSha: "a".repeat(40),
};
const signedPayload = JSON.stringify({
type: "deployment_attestation_bundle",
version: 1,
documents: [
{
subject: deploymentSubject,
type: "deployment_key",
hash: computeBindingDocumentHash({ subject: deploymentSubject, type: "deployment_key", data: deploymentData }),
},
{
subject: versionSubject,
type: "software_version",
hash: computeBindingDocumentHash({ subject: versionSubject, type: "software_version", data: versionData }),
},
],
});
const verify = vi.spyOn(bindingDocumentService as any, "verifyUserPayload").mockResolvedValue(true);
const ownerSignature = {
signer: deployer,
signature: "wallet-signature",
timestamp: new Date().toISOString(),
scope: "bundle" as const,
signedPayload,
};

const deployment = await bindingDocumentService.createBindingDocument({
subject: deploymentSubject,
type: "deployment_key",
data: deploymentData,
ownerSignature,
}, deploymentSubject);
const version = await bindingDocumentService.createBindingDocument({
subject: versionSubject,
type: "software_version",
data: versionData,
ownerSignature,
}, deploymentSubject);

expect(deployment.bindingDocument.signatures[0].signedPayload).toBe(signedPayload);
expect(version.bindingDocument.signatures[0].scope).toBe("bundle");
expect(verify).toHaveBeenCalledWith(
deployer,
"wallet-signature",
`gitw3:deployment:v1:${createHash("sha256").update(signedPayload).digest("base64url")}`,
);
verify.mockRestore();
});

it("rejects unsigned deployment documents", async () => {
const data = {
kind: "deployment_key" as const,
deploymentName: "Production",
environment: "production",
deployerEname: TEST_ENAME,
platformEname: "@platform",
publicKey: "zDeploymentPublicKey",
algorithm: "ECDSA_P256" as const,
};
await expect(bindingDocumentService.createBindingDocument({
subject: "@deployment",
type: "deployment_key",
data,
ownerSignature: {
signer: TEST_ENAME,
signature: computeBindingDocumentHash({ subject: "@deployment", type: "deployment_key", data }),
timestamp: new Date().toISOString(),
},
}, "@deployment")).rejects.toThrow("Invalid owner signature");
});

it("should persist envelope operation logs via dbService after creating a binding document", async () => {
// This test verifies the DB logging infrastructure only; audit emission
// by createBindingDocument itself is out of scope here.
Expand Down
Loading
Loading