Skip to content

feat: new kms api example module branch #6119

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ modules/**/dist/
modules/**/pack-scoped/
coverage
/.direnv/
*.db
5 changes: 5 additions & 0 deletions modules/express-kms-api-example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# KMS API example (REST API)

Based on TDD specification [On-Prem Wallets(https://docs.google.com/document/d/1ku2agwirV3tHCJF350VF_uaVx73D6vu7yUBaDp-cxL0/edit?tab=t.0#heading=h.165ukudv7ejt)]

Made with ExpressJS, Typescript and sqlite3.
37 changes: 37 additions & 0 deletions modules/express-kms-api-example/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "examplekmsapi",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "npx tsx --watch src/app.ts",
"dev-test": "npx tsx --watch src/index-test.ts",
"build": "tsc -p .",
"run": "node dist/src/app.js",
"build-and-run": "tsc -p .; node dist/src/app.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@aws-sdk/client-kms": "^3.810.0",
"@azure/identity": "^4.10.0",
"@azure/keyvault-keys": "^4.9.0",
"body-parser": "^2.2.0",
"express": "^5.1.0",
"sqlite3": "^5.1.7",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"zod": "^3.24.4"
},
"devDependencies": {
"@tsconfig/node22": "^22.0.1",
"@types/express": "^5.0.1",
"@types/node": "^22.15.17",
"@types/swagger-jsdoc": "^6.0.4",
"@types/swagger-ui-express": "^4.1.8",
"tsx": "^4.19.4",
"typescript": "^5.8.3"
}
}
13 changes: 13 additions & 0 deletions modules/express-kms-api-example/scripts/create-tables.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-- TODO: delete this comments! questions for Pranav/Mohammad
-- Some fields with VARCHAR(X) not sure how many characters
-- do we need for a coin.

-- I took the fields from the TDD doc on page 3
-- Not sure also about the prv and pub key length, should we limit them?
CREATE TABLE PRIVATE_KEYS(
id TEXT PRIMARY KEY,
coin VARCHAR(30) NOT NULL,
source VARCHAR(15) CHECK(source IN ('user', 'backup')) NOT NULL,
type VARCHAR(15) CHECK(type IN ('independent', 'tss')) NOT NULL,
prv TEXT NOT NULL,
pub TEXT NOT NULL);
95 changes: 95 additions & 0 deletions modules/express-kms-api-example/src/api/handlers/GET.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { NextFunction, Request, Response } from 'express';
import db from '../../db';
import { KmsInterface } from '../../providers/kms-interface/kmsInterface';

type GetParamsType = {
pub: string;
};

/**
* @openapi
* /key/{pub}:
* get:
* summary: Retrieve a private key stored
* tags:
* - key management service
* parameters:
* - in: path
* name: pub
* required: true
* schema:
* type: string
* description: Public key related to the priv key to retrieve
* - in: query
* name: source
* required: true
* schema:
* type: string
* enum:
* - user
* - backup
* description: The kind of key to retrieve
* responses:
* 200:
* description: Private key retrieved
* content:
* application/json:
* schema:
* type: object
* required:
* - prv
* - pub
* - coin
* - source
* - type
* properties:
* prv:
* type: string
* pub:
* type: string
* source:
* type: string
* enum:
* - user
* - backup
* type:
* type: string
* enum:
* - user
* - backup
* example:
* prv: "MIICXAIBAAKBgH3D4WKfdvhhj9TSGrI0FxAmdfiyfOphuM/kmLMIMKdahZLE5b8YoPL5oIE5NT+157iyQptb7q7qY9nA1jw86Br79FIsi6hLOuAne+1u4jVyJi4PLFAK5gM0c9klGjiunJ+OSH7fX+HQDwykZm20bdEa2fRU4dqT/sRm4Ta1iwAfAgMBAAEC"
* pub: "MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgH3D4WKfdvhhj9TSGrI0FxAmdfiyfOphuM/kmLMIMKdahZLE5b8YoPL5oIE5NT+157iyQptb7q7qY9nA1jw86Br79FIsi6hLOuAne+1u4jVyJi4PLFAU4dqT/sRm4Ta1iwAfAgMBAAE="
* source: "user"
* type: "independent"
* 404:
* description: Private key not found
* 500:
* description: Internal server error
*/
export async function GET(req: Request<GetParamsType>, res: Response, next: NextFunction, kms: KmsInterface): Promise<void> {
const { pub } = req.params;

// fetch from DB
const source = req.query.source;
const data = await db.fetchOne('SELECT (encryptedPrv, kmsKey, type) FROM PRIVATE_KEY WHERE pub = ? AND source = ?', [pub, source]);
if (!data) {
res.status(404);
res.send({ message: `Not Found` })

Check notice

Code scanning / CodeQL

Semicolon insertion Note

Avoid automated semicolon insertion (90% of all statements in
the enclosing function
have an explicit semicolon).

Copilot Autofix

AI about 8 hours ago

To fix the issue, explicitly add a semicolon at the end of the res.send statement on line 78. This ensures consistency with the rest of the function, where all other statements explicitly terminate with semicolons. No additional imports, methods, or definitions are required for this fix.


Suggested changeset 1
modules/express-kms-api-example/src/api/handlers/GET.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/modules/express-kms-api-example/src/api/handlers/GET.ts b/modules/express-kms-api-example/src/api/handlers/GET.ts
--- a/modules/express-kms-api-example/src/api/handlers/GET.ts
+++ b/modules/express-kms-api-example/src/api/handlers/GET.ts
@@ -77,3 +77,3 @@
     res.status(404);
-    res.send({ message: `Not Found` })
+    res.send({ message: `Not Found` });
     return;
EOF
@@ -77,3 +77,3 @@
res.status(404);
res.send({ message: `Not Found` })
res.send({ message: `Not Found` });
return;
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
return;
}

const { encryptedPrv, kmsKey, type } = data;

// fetch prv from kms
const prv = await kms.getKey(kmsKey, encryptedPrv, {})
.catch((err) => {
res.status(500);
res.send({ message: 'Internal server error' });
return; // TODO: test this
});

// TODO: i know that we could chain res.status() with .json but what's the preferred way?
res.status(200);
res.json({ prv, pub, source, type });
}
144 changes: 144 additions & 0 deletions modules/express-kms-api-example/src/api/handlers/POST.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { NextFunction, Request, Response } from 'express';
import db from '../../db';
import { KmsErrorRes, KmsInterface, PostKeyKmsRes } from '../../providers/kms-interface/kmsInterface';
import { ZodPostKeySchema } from '../schemas/postKeySchema';

/**
* @openapi
* /key:
* post:
* summary: Store a new private key
* tags:
* - key management service
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - prv
* - pub
* - coin
* - source
* - type
* properties:
* prv:
* type: string
* pub:
* type: string
* coin:
* type: string
* source:
* type: string
* enum:
* - user
* - backup
* type:
* type: string
* enum:
* - independent
* - mpc
* responses:
* 200:
* description: Successfully stored key
* content:
* application/json:
* schema:
* type: object
* required:
* - prv
* - pub
* - coin
* - source
* - type
* properties:
* keyId:
* type: string
* coin:
* type: string
* source:
* type: string
* enum:
* - user
* - backup
* type:
* type: string
* enum:
* - independent
* - mpc
* pub:
* type: string
* example:
* keyId: "MIICXAIBAAKBgH3D4WKfdvhhj9TSGrI0FxAmdfiyfOphuM/kmLMIMKdahZLE5b8YoPL5oIE5NT+157iyQptb7q7qY9nA1jw86Br79FIsi6hLOuAne+1u4jVyJi4PLFAK5gM0c9klGjiunJ+OSH7fX+HQDwykZm20bdEa2fRU4dqT/sRm4Ta1iwAfAgMBAAEC"
* coin: "sol"
* source: "user"
* type: "tss"
* pub: "MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgH3D4WKfdvhhj9TSGrI0FxAmdfiyfOphuM/kmLMIMKdahZLE5b8YoPL5oIE5NT+157iyQptb7q7qY9nA1jw86Br79FIsi6hLOuAne+1u4jVyJi4PLFAU4dqT/sRm4Ta1iwAfAgMBAAE="
* 400:
* description: Invalid data
* 409:
* description: Duplicate key
* 500:
* description: Internal server error
*/
export async function POST(req: Request, res: Response, next: NextFunction, kms: KmsInterface): Promise<void> {
// parse request
try {
ZodPostKeySchema.parse(req.body);
} catch (e) {
res.status(400);
res.send({ message: 'Invalid data provided from client' });
}

const { prv, pub, coin, source, type } = req.body;

// check for duplicates
const keyObject = await db.fetchAll('SELECT * from PRIVATE_KEYS WHERE pub = ? AND source = ?', [pub, source]);
if (keyObject.length != 0) {
res.status(409);
res.send({ message: `Error: Duplicated Key for source: ${source} and pub: ${pub}` });
return;
}

// db script to fetch kms key from the database, if any exist
let kmsKey = await db.fetchOne('SELECT kmsKey from PRIVATE_KEYS WHERE provider = ? LIMIT 1', ['mock'])
if (!kmsKey) {
kmsKey = await kms.createKmsKey({}).then((kmsRes) => {
if ('code' in kmsRes) {
res.status(kmsRes.code);
res.send({ message: 'Internal server error. Failed to create top-level kms key in KMS' });
return;
}
return kmsRes.kmsKey;
})
Comment on lines +107 to +114

Check warning

Code scanning / CodeQL

Useless assignment to local variable Warning

The value assigned to kmsKey here is unused.

Copilot Autofix

AI about 8 hours ago

To fix the issue, we need to refactor the code to ensure that the value of kmsKey is either properly assigned or the redundant assignment is removed. Since the .then() block is used for error handling and does not propagate a value to kmsKey, we can refactor the code to use await syntax instead of .then(). This will make the code cleaner and ensure that the value of kmsKey is correctly assigned when kms.createKmsKey succeeds.


Suggested changeset 1
modules/express-kms-api-example/src/api/handlers/POST.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/modules/express-kms-api-example/src/api/handlers/POST.ts b/modules/express-kms-api-example/src/api/handlers/POST.ts
--- a/modules/express-kms-api-example/src/api/handlers/POST.ts
+++ b/modules/express-kms-api-example/src/api/handlers/POST.ts
@@ -104,5 +104,6 @@
   // db script to fetch kms key from the database, if any exist
-  let kmsKey = await db.fetchOne('SELECT kmsKey from PRIVATE_KEYS WHERE provider = ? LIMIT 1', ['mock'])
+  let kmsKey = await db.fetchOne('SELECT kmsKey from PRIVATE_KEYS WHERE provider = ? LIMIT 1', ['mock']);
   if (!kmsKey) {
-    kmsKey = await kms.createKmsKey({}).then((kmsRes) => {
+    try {
+      const kmsRes = await kms.createKmsKey({});
       if ('code' in kmsRes) {
@@ -112,4 +113,8 @@
       }
-      return kmsRes.kmsKey;
-    })
+      kmsKey = kmsRes.kmsKey;
+    } catch (error) {
+      res.status(500);
+      res.send({ message: 'Internal server error. Failed to create top-level kms key in KMS' });
+      return;
+    }
   }
EOF
@@ -104,5 +104,6 @@
// db script to fetch kms key from the database, if any exist
let kmsKey = await db.fetchOne('SELECT kmsKey from PRIVATE_KEYS WHERE provider = ? LIMIT 1', ['mock'])
let kmsKey = await db.fetchOne('SELECT kmsKey from PRIVATE_KEYS WHERE provider = ? LIMIT 1', ['mock']);
if (!kmsKey) {
kmsKey = await kms.createKmsKey({}).then((kmsRes) => {
try {
const kmsRes = await kms.createKmsKey({});
if ('code' in kmsRes) {
@@ -112,4 +113,8 @@
}
return kmsRes.kmsKey;
})
kmsKey = kmsRes.kmsKey;
} catch (error) {
res.status(500);
res.send({ message: 'Internal server error. Failed to create top-level kms key in KMS' });
return;
}
}
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
}

// send to kms
const kmsRes: PostKeyKmsRes | KmsErrorRes = await kms.postKey("", prv, {});
if ('code' in kmsRes) { // TODO: type guard
res.status(kmsRes.code);
res.send({ message: 'Internal server error. Failed to encrypt prvaite key in KMS' });
return;
}

// insert into database
// TODO: better catching
await db.run('INSERT INTO PRIVATE_KEYS values (?, ?, ?, ?, ?, ?, ?)', [
pub,
source,
kmsRes.encryptedPrv,
kms.providerName,
kmsRes.topLevelKeyId,
coin,
type,
]).catch((err) => {
res.status(500);
res.send({ message: 'Internal server error' });
return; // TODO: test this
});

res.status(200);
res.json({ coin, source, type, pub });
next();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { z } from 'zod';
import { KeySource, KeyType, MultiSigCoins } from './types';

export const ZodPostKeySchema = z.object({
prv: z.string(), // TODO: min/max length?
pub: z.string(), // TODO: min/max length?
coin: z.enum(MultiSigCoins),
source: z.enum(KeySource),
type: z.enum(KeyType),
});
8 changes: 8 additions & 0 deletions modules/express-kms-api-example/src/api/schemas/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// TODO: add the full list of supported coins
export const MultiSigCoins = ['btc', 'heth'] as const;
export const KeySource = ['user', 'backup'] as const;
export const KeyType = ['independent', 'tss'] as const;

export type MultiSigCoinsType = (typeof MultiSigCoins)[number];
export type KeySourceType = (typeof KeySource)[number];
export type KeyTypeType = (typeof KeyType)[number];
37 changes: 37 additions & 0 deletions modules/express-kms-api-example/src/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import bodyParser from 'body-parser';
import express from 'express';
import swaggerJSDoc from 'swagger-jsdoc';
import swaggerUi from 'swagger-ui-express';
import { GET as keyGET } from './api/handlers/GET';
import { POST as keyPOST } from './api/handlers/POST';
import { checkApiKeyMiddleware } from './middlewares/authApiKeys';
import { swaggerOptions } from './swagger';
import { mockKmsProvider } from './providers/mock/mock-kms';
import db from './db';

// TODO: move to proper .env
// Add note about the port to the README
// Or hardcode it
const app = express();
const PORT = '3000';
const kmsInterface = new mockKmsProvider(); // TODO: use a config file to determine the provider, perhaps globally?

const swaggerSpec = swaggerJSDoc(swaggerOptions);

app.use(bodyParser.json());
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
app.use(checkApiKeyMiddleware);

// TODO: create router for keys controllers,
// I added the controllers calls directly here.
app.post('/key', (req, res, next) => keyPOST(req, res, next, kmsInterface));
app.get('/key/:pub', (req, res, next) => keyGET(req, res, next, kmsInterface));
app.get('/openapi.json', (req, res) => {
res.setHeader('Content-Type', 'application/json');
res.send(swaggerSpec);
});

app.listen(PORT, () => {
db.setup();
console.log(`KMS API example listening on port ${PORT}`);
});
Loading
Loading