Skip to content
Open
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
7 changes: 7 additions & 0 deletions oracle/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,10 @@ ORACLE_HIGH_VALUE_THRESHOLD_XLM=10000
# Minimum prize amount (in XLM) for a draw request to be classified as MEDIUM priority (Bull priority 5)
# Must be less than ORACLE_HIGH_VALUE_THRESHOLD_XLM; falls back to defaults if not
ORACLE_MED_VALUE_THRESHOLD_XLM=1000

# Provider for keys: env, aws-kms, gcp-kms
KEY_PROVIDER=env

# GCP KMS Config
GCP_KMS_PROJECT=
GCP_KMS_KEY_PATH=
13 changes: 5 additions & 8 deletions oracle/src/keys/key-provider.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,19 +82,16 @@ export class KeyProviderFactory {
}

private static createGcpKmsProvider(configService: ConfigService): GcpKmsKeyProvider {
const projectId = configService.get<string>('GCP_PROJECT_ID');
const locationId = configService.get<string>('GCP_LOCATION_ID', 'global');
const keyRingId = configService.get<string>('GCP_KEY_RING_ID');
const keyId = configService.get<string>('GCP_KEY_ID');
const keyVersion = configService.get<string>('GCP_KEY_VERSION', '1');
const projectId = configService.get<string>('GCP_KMS_PROJECT');
const keyPath = configService.get<string>('GCP_KMS_KEY_PATH');

if (!projectId || !keyRingId || !keyId) {
if (!projectId || !keyPath) {
throw new Error(
'GCP_PROJECT_ID, GCP_KEY_RING_ID, and GCP_KEY_ID environment variables are required for GCP KMS provider',
'GCP_KMS_PROJECT and GCP_KMS_KEY_PATH environment variables are required for GCP KMS provider',
);
}

this.logger.log('Using Google Cloud KMS for secure key management');
return new GcpKmsKeyProvider(projectId, locationId, keyRingId, keyId, keyVersion);
return new GcpKmsKeyProvider(projectId, keyPath);
}
}
111 changes: 111 additions & 0 deletions oracle/src/keys/providers/gcp-kms-key.provider.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { GcpKmsKeyProvider } from './gcp-kms-key.provider';

const mockGetPublicKey = jest.fn();
const mockAsymmetricSign = jest.fn();
const mockGetCryptoKeyVersion = jest.fn();

jest.mock('@google-cloud/kms', () => {
return {
KeyManagementServiceClient: jest.fn().mockImplementation(() => {
return {
getPublicKey: mockGetPublicKey,
asymmetricSign: mockAsymmetricSign,
getCryptoKeyVersion: mockGetCryptoKeyVersion,
};
}),
};
});

describe('GcpKmsKeyProvider', () => {
let provider: GcpKmsKeyProvider;
const testKeyPath = 'projects/test-project/locations/global/keyRings/test-ring/cryptoKeys/test-key/cryptoKeyVersions/1';

beforeEach(() => {
jest.clearAllMocks();
provider = new GcpKmsKeyProvider('test-project', testKeyPath);
});

describe('initialization', () => {
it('throws error if project ID or key path are missing', () => {
expect(() => new GcpKmsKeyProvider('', 'key-path')).toThrow();
expect(() => new GcpKmsKeyProvider('project', '')).toThrow();
});
});

describe('getPublicKey / getPublicKeyBuffer', () => {
it('loads and parses the public key successfully', async () => {
const pemKey = `-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n-----END PUBLIC KEY-----`;
mockGetPublicKey.mockResolvedValue([{ pem: pemKey }]);

const pubKeyString = await provider.getPublicKey();
expect(mockGetPublicKey).toHaveBeenCalledWith({
name: testKeyPath,
});
expect(pubKeyString).toBeDefined();

const buffer = await provider.getPublicKeyBuffer();
expect(buffer).toBeInstanceOf(Buffer);
});

it('caches the public key', async () => {
const pemKey = `-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n-----END PUBLIC KEY-----`;
mockGetPublicKey.mockResolvedValue([{ pem: pemKey }]);

await provider.getPublicKey();
await provider.getPublicKey(); // Should use cache
expect(mockGetPublicKey).toHaveBeenCalledTimes(1);
});

it('throws error if getPublicKey fails', async () => {
mockGetPublicKey.mockRejectedValue(new Error('Network error'));
await expect(provider.getPublicKey()).rejects.toThrow('Failed to retrieve public key from GCP KMS');
});
});

describe('sign', () => {
it('signs data successfully', async () => {
const mockSignature = Buffer.from('test-signature');
mockAsymmetricSign.mockResolvedValue([{ signature: mockSignature }]);

const dataToSign = Buffer.from('test-data');
const signature = await provider.sign(dataToSign);

expect(mockAsymmetricSign).toHaveBeenCalledWith({
name: testKeyPath,
data: dataToSign,
});
expect(signature).toEqual(mockSignature);
});

it('throws error if asymmetricSign fails', async () => {
mockAsymmetricSign.mockRejectedValue(new Error('Signing error'));
const dataToSign = Buffer.from('test-data');
await expect(provider.sign(dataToSign)).rejects.toThrow('Failed to sign data with GCP KMS');
});
});

describe('getProviderHealth', () => {
it('returns healthy when getCryptoKeyVersion succeeds', async () => {
mockGetCryptoKeyVersion.mockResolvedValue([{ name: testKeyPath }]);
const health = await provider.getProviderHealth();
expect(health.status).toBe('healthy');
expect(health.activeKeyId).toBe(testKeyPath);
});

it('returns permission_denied when IAM fails', async () => {
const error: any = new Error('7 PERMISSION_DENIED');
error.code = 7;
mockGetCryptoKeyVersion.mockRejectedValue(error);
const health = await provider.getProviderHealth();
expect(health.status).toBe('permission_denied');
});

it('returns unavailable when network fails', async () => {
const error: any = new Error('14 UNAVAILABLE');
error.code = 14;
mockGetCryptoKeyVersion.mockRejectedValue(error);
const health = await provider.getProviderHealth();
expect(health.status).toBe('unavailable');
});
});
});
11 changes: 4 additions & 7 deletions oracle/src/keys/providers/gcp-kms-key.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,15 @@ export class GcpKmsKeyProvider implements KeyProvider {

constructor(
projectId: string,
locationId: string,
keyRingId: string,
keyId: string,
keyVersion: string = '1',
keyPath: string,
) {
if (!projectId || !locationId || !keyRingId || !keyId) {
if (!projectId || !keyPath) {
throw new Error(
'GCP project, location, keyRing, and key IDs are required for GcpKmsKeyProvider',
'GCP_KMS_PROJECT and GCP_KMS_KEY_PATH are required for GcpKmsKeyProvider',
);
}

this.keyVersionName = `projects/${projectId}/locations/${locationId}/keyRings/${keyRingId}/cryptoKeys/${keyId}/cryptoKeyVersions/${keyVersion}`;
this.keyVersionName = keyPath;

try {
// Lazy load Google Cloud SDK to avoid requiring it when not using GCP KMS
Expand Down
Loading