Skip to content

Commit

Permalink
Introduces GCP signer on @mysten/signers (#20473)
Browse files Browse the repository at this point in the history
## Description 

Describe the changes or additions included in this PR.

## Test plan 

How did you test the new or updated feature?

---

## Release notes

Check each box that your changes affect. If none of the boxes relate to
your changes, release notes aren't required.

For each box you select, include information after the relevant heading
that describes the impact of your changes that a user might notice and
any actions they must take to implement updates.

- [ ] Protocol: 
- [ ] Nodes (Validators and Full nodes): 
- [ ] Indexer: 
- [ ] JSON-RPC: 
- [ ] GraphQL: 
- [ ] CLI: 
- [ ] Rust SDK:
- [ ] REST API:
  • Loading branch information
manolisliolios authored Dec 7, 2024
1 parent e2d7ef0 commit 2349920
Show file tree
Hide file tree
Showing 15 changed files with 792 additions and 128 deletions.
5 changes: 5 additions & 0 deletions .changeset/polite-dryers-collect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@mysten/signers': minor
---

Introduces GCP KMS signer at `@mysten/signers/gcp`
10 changes: 10 additions & 0 deletions .github/workflows/turborepo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ jobs:
uses: jetli/wasm-pack-action@0d096b08b4e5a7de8c28de67e11e945404e9eefa # [email protected]
with:
version: 'latest'
- name: configure gcp/gke service user auth
uses: google-github-actions/auth@v1
with:
credentials_json: ${{ secrets.GKE_TEST_KMS_SVCUSER_CREDENTIALS }}
- name: Build
run: pnpm turbo build
- name: Test
Expand All @@ -67,6 +71,12 @@ jobs:
AWS_REGION: ${{ vars.AWS_KMS_AWS_REGION }}
AWS_KMS_KEY_ID: ${{ secrets.AWS_KMS_TEST_KMS_KEY_ID }}
E2E_AWS_KMS_TEST_ENABLE: "true"
GOOGLE_PROJECT_ID: ${{ secrets.GOOGLE_PROJECT_ID }}
GOOGLE_LOCATION: ${{ secrets.GOOGLE_LOCATION }}
GOOGLE_KEYRING: ${{ secrets.GOOGLE_KEYRING }}
GOOGLE_KEY_NAME: ${{ secrets.GOOGLE_KEY_NAME }}
GOOGLE_KEY_NAME_VERSION: ${{ secrets.GOOGLE_KEY_NAME_VERSION }}
E2E_GCP_KMS_TEST_ENABLE: "true"
run: pnpm turbo test

# Pack wallet extension and upload it as an artifact for easy developer use:
Expand Down
450 changes: 389 additions & 61 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ packages:
- '!sdk/typescript/graphql/schemas/2024.1'
- '!sdk/typescript/graphql/schemas/2024.4'
- '!sdk/signers/aws'
- '!sdk/signers/gcp'
6 changes: 6 additions & 0 deletions sdk/signers/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,9 @@ export AWS_ACCESS_KEY_ID=""
export AWS_SECRET_ACCESS_KEY=""
export AWS_REGION=""
export AWS_KMS_KEY_ID=""

export GOOGLE_PROJECT_ID=""
export GOOGLE_LOCATION=""
export GOOGLE_KEYRING=""
export GOOGLE_KEY_NAME=""
export GOOGLE_KEY_NAME_VERSION="1"
69 changes: 68 additions & 1 deletion sdk/signers/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Sui KMS Signers

The Sui KMS Signers package provides a set of tools for securely signing transactions using Key
Management Services (KMS) like AWS KMS.
Management Services (KMS) like AWS KMS and GCP KMS.

## Table of Contents

Expand All @@ -11,6 +11,12 @@ Management Services (KMS) like AWS KMS.
- [fromKeyId](#fromkeyid)
- [Parameters](#parameters)
- [Examples](#examples)
- [GCP KMS Signer](#gcp-kms-signer)
- [Usage](#usage-1)
- [API](#api-1)
- [fromOptions](#fromoptions)
- [Parameters](#parameters-1)
- [Examples](#examples-1)

## AWS KMS Signer

Expand Down Expand Up @@ -73,3 +79,64 @@ Returns
An instance of AwsKmsSigner.

**Notice**: AWS Signer requires Node >=20 due to dependency on `crypto`

## GCP KMS Signer

The GCP KMS Signer allows you to leverage Google Cloud's Key Management Service to sign Sui
transactions.

### Usage

#### fromOptions

Create a GCP KMS signer from the provided options. This method initializes the signer with the
necessary GCP credentials and configuration, allowing it to interact with GCP KMS to perform
cryptographic operations.

##### Parameters

- `options`
**[object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** An
object containing GCP credentials and configuration.
- `projectId`
**[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)**
The GCP project ID.
- `location`
**[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)**
The GCP location.
- `keyRing`
**[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)**
The GCP key ring.
- `cryptoKey`
**[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)**
The GCP crypto key.
- `cryptoKeyVersion`
**[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)**
The GCP crypto key version.

##### Examples

```typescript
const signer = await GcpKmsSigner.fromOptions({
projectId: 'your-google-project-id',
location: 'your-google-location',
keyRing: 'your-google-keyring',
cryptoKey: 'your-google-key-name',
cryptoKeyVersion: 'your-google-key-name-version',
});

// Retrieve the public key and get the Sui address
const publicKey = signer.getPublicKey();
console.log(publicKey.toSuiAddress());

// Define a test message
const testMessage = 'Hello, GCP KMS Signer!';
const messageBytes = new TextEncoder().encode(testMessage);

// Sign the test message
const { signature } = await signer.signPersonalMessage(messageBytes);

// Verify the signature against the public key
const isValid = await publicKey.verifyPersonalMessage(messageBytes, signature);
console.log(isValid); // Should print true if the signature is valid
```
6 changes: 6 additions & 0 deletions sdk/signers/gcp/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"private": true,
"import": "../dist/esm/gcp/index.js",
"main": "../dist/cjs/gcp/index.js",
"sideEffects": false
}
6 changes: 6 additions & 0 deletions sdk/signers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
"./aws": {
"import": "./dist/esm/aws/index.js",
"require": "./dist/cjs/aws/index.js"
},
"./gcp": {
"import": "./dist/esm/gcp/index.js",
"require": "./dist/cjs/gcp/index.js"
}
},
"sideEffects": false,
Expand All @@ -18,6 +22,7 @@
"README.md",
"aws",
"dist",
"gcp",
"src"
],
"scripts": {
Expand Down Expand Up @@ -48,6 +53,7 @@
"vitest": "^2.0.1"
},
"dependencies": {
"@google-cloud/kms": "^4.5.0",
"@mysten/sui": "workspace:*",
"@noble/curves": "^1.4.2",
"@noble/hashes": "^1.4.0",
Expand Down
28 changes: 2 additions & 26 deletions sdk/signers/src/aws/aws-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
import { Secp256k1PublicKey } from '@mysten/sui/keypairs/secp256k1';
import { Secp256r1PublicKey } from '@mysten/sui/keypairs/secp256r1';
import { fromBase64 } from '@mysten/sui/utils';
import { ASN1Construction, ASN1TagClass, DERElement } from 'asn1-ts';

import { publicKeyFromDER } from '../utils/utils.js';
import { AwsClient } from './aws4fetch.js';
import { compressPublicKeyClamped } from './utils.js';

interface KmsCommands {
Sign: {
Expand Down Expand Up @@ -66,30 +65,7 @@ export class AwsKmsClient extends AwsClient {
throw new Error('Public Key not found for the supplied `keyId`');
}

const publicKey = fromBase64(publicKeyResponse.PublicKey);

const encodedData: Uint8Array = publicKey;
const derElement = new DERElement();
derElement.fromBytes(encodedData);

// Validate the ASN.1 structure of the public key
if (
!(
derElement.tagClass === ASN1TagClass.universal &&
derElement.construction === ASN1Construction.constructed
)
) {
throw new Error('Unexpected ASN.1 structure');
}

const components = derElement.components;
const publicKeyElement = components[1];

if (!publicKeyElement) {
throw new Error('Public Key not found in the DER structure');
}

const compressedKey = compressPublicKeyClamped(publicKeyElement.bitString);
const compressedKey = publicKeyFromDER(fromBase64(publicKeyResponse.PublicKey));

switch (publicKeyResponse.KeySpec) {
case 'ECC_NIST_P256':
Expand Down
41 changes: 2 additions & 39 deletions sdk/signers/src/aws/aws-kms-signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@
import type { PublicKey, SignatureFlag } from '@mysten/sui/cryptography';
import { SIGNATURE_FLAG_TO_SCHEME, Signer } from '@mysten/sui/cryptography';
import { fromBase64, toBase64 } from '@mysten/sui/utils';
import { secp256r1 } from '@noble/curves/p256';
import { secp256k1 } from '@noble/curves/secp256k1';
import { DERElement } from 'asn1-ts';

import { getConcatenatedSignature } from '../utils/utils.js';
import type { AwsClientOptions } from './aws-client.js';
import { AwsKmsClient } from './aws-client.js';

Expand Down Expand Up @@ -82,7 +80,7 @@ export class AwsKmsSigner extends Signer {
});

// Concatenate the signature components into a compact form
return this.#getConcatenatedSignature(fromBase64(signResponse.Signature));
return getConcatenatedSignature(fromBase64(signResponse.Signature), this.getKeyScheme());
}

/**
Expand All @@ -93,41 +91,6 @@ export class AwsKmsSigner extends Signer {
throw new Error('KMS Signer does not support sync signing');
}

/**
* Generates a concatenated signature from a DER-encoded signature.
*
* This signature format is consumable by Sui's `toSerializedSignature` method.
*
* @param signature - A `Uint8Array` representing the DER-encoded signature.
* @returns A `Uint8Array` containing the concatenated signature in compact form.
*
* @throws {Error} If the input signature is invalid or cannot be processed.
*/
#getConcatenatedSignature(signature: Uint8Array): Uint8Array {
if (!signature || signature.length === 0) {
throw new Error('Invalid signature');
}

// Initialize a DERElement to parse the DER-encoded signature
const derElement = new DERElement();
derElement.fromBytes(signature);

const [r, s] = derElement.toJSON() as [string, string];

switch (this.getKeyScheme()) {
case 'Secp256k1':
return new secp256k1.Signature(BigInt(r), BigInt(s)).normalizeS().toCompactRawBytes();
case 'Secp256r1':
return new secp256r1.Signature(BigInt(r), BigInt(s)).normalizeS().toCompactRawBytes();
}

// Create a Secp256k1Signature using the extracted r and s values
const secp256k1Signature = new secp256k1.Signature(BigInt(r), BigInt(s));

// Normalize the signature and convert it to compact raw bytes
return secp256k1Signature.normalizeS().toCompactRawBytes();
}

/**
* Prepares the signer by fetching and setting the public key from AWS KMS.
* It is recommended to initialize an `AwsKmsSigner` instance using this function.
Expand Down
Loading

0 comments on commit 2349920

Please sign in to comment.