Skip to content
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
3 changes: 2 additions & 1 deletion src/drivers/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export const fetchAdminLsCommand = async <T>(

export const submitPublicKey = async <T>(
authn: Authn,
args: { publicKey: string; requestId: string },
args: { publicKey: string; requestId: string; signature?: string },
debug?: boolean
) =>
authFetch<T>(authn, {
Expand All @@ -126,6 +126,7 @@ export const submitPublicKey = async <T>(
body: JSON.stringify({
requestId: args.requestId,
publicKey: args.publicKey,
...(args.signature ? { signature: args.signature } : {}),
}),
debug,
});
Expand Down
76 changes: 76 additions & 0 deletions src/plugins/self-hosted/kms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/** Copyright © 2024-present P0 Security

This file is part of @p0security/cli

@p0security/cli is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3 of the License.

@p0security/cli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
**/
import { asyncSpawn } from "../../common/subprocess";
import { print2 } from "../../drivers/stdio";
import { gcloudCommandArgs } from "../google/util";

/**
* Signs the given data string using a Google Cloud KMS asymmetric signing key.
*
* Requires the gcloud CLI to be installed and authenticated via `gcloud auth login`.
*
* @param data - The data to sign (the raw OpenSSH public key string).
* @param kmsKeyResourceName - Full KMS key version resource name, e.g.
* `projects/P/locations/L/keyRings/R/cryptoKeys/K/cryptoKeyVersions/V`
* @returns Base64-encoded signature as returned by the KMS API.
*/
export const signWithKms = async (
data: string,
kmsKeyResourceName: string,
options?: { debug?: boolean }
): Promise<string> => {
const debug = options?.debug ?? false;

// Force debug=false to avoid printing the access token
const { command: accessTokenCommand, args: accessTokenArgs } =
gcloudCommandArgs(["auth", "print-access-token"]);
const accessToken = await asyncSpawn(
{ debug: false },
accessTokenCommand,
accessTokenArgs
);

if (debug) {
print2(
`Signing public key with KMS key: ${kmsKeyResourceName} (token: ${accessToken.slice(0, 10)}...)`
);
}

const encodedData = Buffer.from(data, "utf-8").toString("base64");

const url = `https://cloudkms.googleapis.com/v1/${kmsKeyResourceName}:asymmetricSign`;
const response = await fetch(url, {
method: "POST",
body: JSON.stringify({ data: encodedData }),
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});

if (!response.ok) {
if (debug) {
print2(`KMS HTTP error ${response.status}: ${await response.text()}`);
}
if (response.status === 401) {
throw `Authentication failed. Please login to Google Cloud CLI with 'gcloud auth login'`;
}
throw `KMS signing failed.`;
}

const result: { signature: string } = await response.json();

if (debug) {
print2(`KMS signature obtained (${result.signature.length} chars)`);
}

return result.signature;
};
18 changes: 16 additions & 2 deletions src/plugins/self-hosted/ssh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ This file is part of @p0security/cli
You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
**/
import { isSudoCommand } from "../../commands/shared/ssh";
import { submitPublicKey } from "../../drivers/api";
import { fetchIntegrationConfig, submitPublicKey } from "../../drivers/api";
import { SshConfig } from "../../plugins/ssh/types";
import { SshProvider } from "../../types/ssh";
import { getAppName, getOperatingSystem } from "../../util";
import { signWithKms } from "./kms";
import { SelfHostedSshPermissionSpec, SelfHostedSshRequest } from "./types";

const PROPAGATION_TIMEOUT_LIMIT_MS = 2 * 60 * 1000;
Expand Down Expand Up @@ -57,7 +59,19 @@ export const selfHostedSshProvider: SshProvider<
},

async submitPublicKey(authn, _request, requestId, publicKey, debug) {
await submitPublicKey(authn, { publicKey, requestId }, debug);
const configDoc = await fetchIntegrationConfig<{ config: SshConfig }>(
authn,
"ssh",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be a little more convenient from a backend perspective to have this in the self-hosted integration config doc instead.

debug
);
const kmsKeyResourceName = configDoc?.config?.kmsKeyResourceName;

let signature: string | undefined;
if (kmsKeyResourceName) {
signature = await signWithKms(publicKey, kmsKeyResourceName, { debug });
}

await submitPublicKey(authn, { publicKey, requestId, signature }, debug);
},

proxyCommand: (request, port) => {
Expand Down
1 change: 1 addition & 0 deletions src/plugins/ssh/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type SshItemConfig = {

export type SshConfig = {
"iam-write": Record<string, SshItemConfig>;
kmsKeyResourceName?: string;
};

export type CommonSshPermissionSpec = {
Expand Down
Loading