@axinom/mosaic-message-bus 0.32.0-rc.8 → 0.33.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/broker.js +5 -2
  2. package/dist/broker.js.map +1 -1
  3. package/dist/common/constants.d.ts +27 -0
  4. package/dist/common/constants.d.ts.map +1 -1
  5. package/dist/common/constants.js +28 -1
  6. package/dist/common/constants.js.map +1 -1
  7. package/dist/generated/key-service.d.ts +438 -0
  8. package/dist/generated/key-service.d.ts.map +1 -0
  9. package/dist/generated/key-service.js +162 -0
  10. package/dist/generated/key-service.js.map +1 -0
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +1 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/middleware/index.d.ts +1 -0
  16. package/dist/middleware/index.d.ts.map +1 -1
  17. package/dist/middleware/index.js +1 -0
  18. package/dist/middleware/index.js.map +1 -1
  19. package/dist/middleware/validate-signed-event-middleware.d.ts +36 -0
  20. package/dist/middleware/validate-signed-event-middleware.d.ts.map +1 -0
  21. package/dist/middleware/validate-signed-event-middleware.js +57 -0
  22. package/dist/middleware/validate-signed-event-middleware.js.map +1 -0
  23. package/dist/publication.d.ts.map +1 -1
  24. package/dist/publication.js +3 -2
  25. package/dist/publication.js.map +1 -1
  26. package/dist/rascal-config-builder.d.ts +9 -1
  27. package/dist/rascal-config-builder.d.ts.map +1 -1
  28. package/dist/rascal-config-builder.js +1 -0
  29. package/dist/rascal-config-builder.js.map +1 -1
  30. package/dist/signing/cache-public-signing-keys.d.ts +37 -0
  31. package/dist/signing/cache-public-signing-keys.d.ts.map +1 -0
  32. package/dist/signing/cache-public-signing-keys.js +81 -0
  33. package/dist/signing/cache-public-signing-keys.js.map +1 -0
  34. package/dist/signing/event-signing-errors.d.ts +30 -0
  35. package/dist/signing/event-signing-errors.d.ts.map +1 -0
  36. package/dist/signing/event-signing-errors.js +33 -0
  37. package/dist/signing/event-signing-errors.js.map +1 -0
  38. package/dist/signing/index.d.ts +4 -0
  39. package/dist/signing/index.d.ts.map +1 -0
  40. package/dist/signing/index.js +20 -0
  41. package/dist/signing/index.js.map +1 -0
  42. package/dist/signing/register-public-signing-key.d.ts +16 -0
  43. package/dist/signing/register-public-signing-key.d.ts.map +1 -0
  44. package/dist/signing/register-public-signing-key.js +44 -0
  45. package/dist/signing/register-public-signing-key.js.map +1 -0
  46. package/dist/signing/signing-cache.d.ts +25 -0
  47. package/dist/signing/signing-cache.d.ts.map +1 -0
  48. package/dist/signing/signing-cache.js +46 -0
  49. package/dist/signing/signing-cache.js.map +1 -0
  50. package/dist/types/index.d.ts +1 -1
  51. package/dist/types/index.d.ts.map +1 -1
  52. package/dist/types/index.js +1 -1
  53. package/dist/types/index.js.map +1 -1
  54. package/dist/types/signing.d.ts +30 -0
  55. package/dist/types/signing.d.ts.map +1 -0
  56. package/dist/types/{signing-details.js → signing.js} +1 -1
  57. package/dist/types/signing.js.map +1 -0
  58. package/package.json +16 -5
  59. package/src/common/constants.ts +32 -0
  60. package/src/generated/key-service.ts +459 -0
  61. package/src/index.ts +1 -0
  62. package/src/middleware/index.ts +1 -0
  63. package/src/middleware/validate-signed-event-middleware.spec.ts +679 -0
  64. package/src/middleware/validate-signed-event-middleware.ts +136 -0
  65. package/src/publication.ts +9 -5
  66. package/src/rascal-config-builder.spec.ts +1 -33
  67. package/src/rascal-config-builder.ts +11 -1
  68. package/src/signing/cache-public-signing-keys.graphql +10 -0
  69. package/src/signing/cache-public-signing-keys.ts +115 -0
  70. package/src/signing/event-signing-errors.ts +35 -0
  71. package/src/signing/index.ts +3 -0
  72. package/src/signing/register-public-signing-key.graphql +10 -0
  73. package/src/signing/register-public-signing-key.spec.ts +95 -0
  74. package/src/signing/register-public-signing-key.ts +59 -0
  75. package/src/signing/signing-cache.ts +49 -0
  76. package/src/tests/utils/create-builder.ts +34 -0
  77. package/src/tests/utils/index.ts +1 -0
  78. package/src/types/index.ts +1 -1
  79. package/src/types/signing.ts +50 -0
  80. package/dist/types/signing-details.d.ts +0 -3
  81. package/dist/types/signing-details.d.ts.map +0 -1
  82. package/dist/types/signing-details.js.map +0 -1
  83. package/src/types/signing-details.ts +0 -8
@@ -0,0 +1,136 @@
1
+ import { MosaicError, validateSignature } from '@axinom/mosaic-service-common';
2
+ import { AckOrNack } from 'rascal';
3
+ import {
4
+ MOSAIC_SIGNING_SIGNATURE,
5
+ MOSAIC_SIGNING_SIGNATURE_KEY_VERSION,
6
+ } from '../common';
7
+ import { RascalConfigBuilder } from '../rascal-config-builder';
8
+ import { EventSigningErrors, getCachedPublicKey } from '../signing';
9
+ import {
10
+ GetEventSigningTokenFunc,
11
+ MessageEnvelope,
12
+ MessageInfo,
13
+ OnMessageMiddleware,
14
+ PublicSigningKey,
15
+ } from '../types';
16
+
17
+ /**
18
+ * Parameters object expected by the `validateSignedEventMiddleware`
19
+ */
20
+ export interface ValidateSignedEventParams {
21
+ /**
22
+ * Used to determine which events the service is expecting to
23
+ * receive. Passing a list of all builders used by `setupMessagingBroker` is recommended.
24
+ */
25
+ builders: RascalConfigBuilder[];
26
+ /**
27
+ * Base URL of Mosaic Key Service
28
+ */
29
+ keyServiceBaseUrl: string;
30
+ /**
31
+ * Function to retrieve authorization token result to make a
32
+ * request to the Key Service
33
+ */
34
+ getTokenCallback: GetEventSigningTokenFunc;
35
+ /**
36
+ * An optional array of trusted public keys that are not registered with the
37
+ * Key service. Could be used by customizable services if event signing is
38
+ * enabled for customizable services. If not specified - validation of events
39
+ * originating from customizable services will be skipped, assuming that event
40
+ * signing is not enabled for them. Alternatively, might useful for specifying
41
+ * services own key, if registration is delayed.
42
+ */
43
+ extraTrustedPublicKeys?: PublicSigningKey[];
44
+ }
45
+
46
+ /**
47
+ * Messaging middleware to validate signed events, making sure they originated
48
+ * from a trustworthy source.
49
+ */
50
+ export const validateSignedEventMiddleware = ({
51
+ builders,
52
+ keyServiceBaseUrl,
53
+ getTokenCallback,
54
+ extraTrustedPublicKeys = [],
55
+ }: ValidateSignedEventParams): OnMessageMiddleware => {
56
+ return async (
57
+ envelope: MessageEnvelope,
58
+ message: MessageInfo,
59
+ ackOrNack: AckOrNack,
60
+ next?: OnMessageMiddleware,
61
+ ): Promise<void> => {
62
+ const eventBuilder = builders.find(
63
+ (b) => b.info.messageType === envelope.message_type,
64
+ );
65
+ if (!eventBuilder) {
66
+ throw new MosaicError(EventSigningErrors.UnexpectedMessageType);
67
+ }
68
+
69
+ if (eventBuilder.info.action !== 'event') {
70
+ // Commands do not require signature validation
71
+ await next?.(envelope, message, ackOrNack);
72
+ return;
73
+ }
74
+
75
+ const serviceId =
76
+ eventBuilder.info.serviceId ?? message.fields.routingKey?.split('.')[0];
77
+ if (!serviceId) {
78
+ throw new MosaicError(EventSigningErrors.ServiceIdNotFound);
79
+ }
80
+
81
+ const customizableTrustedKeys = extraTrustedPublicKeys.filter(
82
+ (x) => !x.serviceId.startsWith('ax-'),
83
+ );
84
+ if (customizableTrustedKeys.length === 0 && !serviceId.startsWith('ax-')) {
85
+ // Event signing not enabled for customizable services, skipping
86
+ // validation of event from customizable service.
87
+ await next?.(envelope, message, ackOrNack);
88
+ return;
89
+ }
90
+
91
+ // Assuming that if at least one custom public key is set for customizable
92
+ // services - then all customizable services per environment should be using
93
+ // message signing
94
+
95
+ const signature = message.properties.headers[MOSAIC_SIGNING_SIGNATURE];
96
+ const version =
97
+ message.properties.headers[MOSAIC_SIGNING_SIGNATURE_KEY_VERSION];
98
+ if (!signature || !version) {
99
+ throw new MosaicError({
100
+ ...EventSigningErrors.SigningHeadersMissing,
101
+ details: { signature, version },
102
+ });
103
+ }
104
+
105
+ const publicKey =
106
+ extraTrustedPublicKeys.find(
107
+ (x) => x.serviceId === serviceId && x.version === version,
108
+ )?.key ||
109
+ (await getCachedPublicKey(
110
+ serviceId,
111
+ version,
112
+ builders,
113
+ keyServiceBaseUrl,
114
+ getTokenCallback,
115
+ ));
116
+
117
+ const isValid = validateSignature(
118
+ message.unparsedEnvelope.toString(),
119
+ signature,
120
+ publicKey,
121
+ );
122
+ if (!isValid) {
123
+ throw new MosaicError({
124
+ ...EventSigningErrors.SignatureValidationFailed,
125
+ details: {
126
+ serviceId,
127
+ version,
128
+ signature,
129
+ publicKey,
130
+ },
131
+ });
132
+ }
133
+
134
+ await next?.(envelope, message, ackOrNack);
135
+ };
136
+ };
@@ -6,7 +6,11 @@ import {
6
6
  import { mergeDeepRight } from 'ramda';
7
7
  import { BrokerAsPromised, PublicationSession } from 'rascal';
8
8
  import { v4 as uuid } from 'uuid';
9
- import { AggregateMessageType } from './common';
9
+ import {
10
+ AggregateMessageType,
11
+ MOSAIC_SIGNING_SIGNATURE,
12
+ MOSAIC_SIGNING_SIGNATURE_KEY_VERSION,
13
+ } from './common';
10
14
  import {
11
15
  MessageEnvelope,
12
16
  MessageEnvelopeOverrides,
@@ -81,8 +85,8 @@ export default class Publication {
81
85
  eventSigning: SigningDetails | undefined,
82
86
  ): {
83
87
  headers?: {
84
- ['x-mosaic-signature']: string;
85
- ['x-mosaic-signature-key-version']: number;
88
+ [MOSAIC_SIGNING_SIGNATURE]: string;
89
+ [MOSAIC_SIGNING_SIGNATURE_KEY_VERSION]: number;
86
90
  };
87
91
  body: string | MessageEnvelope<unknown>;
88
92
  } {
@@ -96,8 +100,8 @@ export default class Publication {
96
100
  );
97
101
  return {
98
102
  headers: {
99
- ['x-mosaic-signature']: signature,
100
- ['x-mosaic-signature-key-version']:
103
+ [MOSAIC_SIGNING_SIGNATURE]: signature,
104
+ [MOSAIC_SIGNING_SIGNATURE_KEY_VERSION]:
101
105
  eventSigning.rmqEventSigningKeyVersion,
102
106
  },
103
107
  body: stringifiedValue,
@@ -1,10 +1,10 @@
1
1
  /* eslint-disable no-console */
2
- import { MessagingSettings } from '@axinom/mosaic-message-bus-abstractions';
3
2
  import { stub } from 'jest-auto-stub';
4
3
  import 'jest-extended';
5
4
  import { BrokerProxy } from './broker';
6
5
  import { MessageHandler } from './message-handler';
7
6
  import { RascalConfigBuilder } from './rascal-config-builder';
7
+ import { createBuilder } from './tests/utils';
8
8
 
9
9
  describe('RascalConfigBuilder', () => {
10
10
  let builder: RascalConfigBuilder;
@@ -13,38 +13,6 @@ describe('RascalConfigBuilder', () => {
13
13
  ) => MessageHandler<unknown> = () => {
14
14
  return stub<MessageHandler<unknown>>();
15
15
  };
16
-
17
- const createBuilder = (
18
- infoOverrides: Partial<MessagingSettings> = {},
19
- ): RascalConfigBuilder => {
20
- return new RascalConfigBuilder(
21
- {
22
- action: 'event',
23
- aggregateType: 'test',
24
- messageType: 'TestKey',
25
- queue: 'test-queue',
26
- routingKey: 'test.key',
27
- ...infoOverrides,
28
- },
29
- {
30
- isDev: false,
31
- serviceId: 'test-service-id',
32
- rmqProtocol: 'test-protocol',
33
- rmqVHost: 'test-vhost',
34
- rmqHost: 'test-host',
35
- rmqPort: 123,
36
- rmqUser: 'test-user',
37
- rmqPassword: 'test-pw',
38
- rmqVHostAssert: true,
39
- rmqChannelMax: 5,
40
- rmqMgmtHost: 'test',
41
- rmqMgmtPort: 1234,
42
- rmqMgmtProtocol: 'amqp',
43
- rmqEventSigningKeyVersion: 1,
44
- rmqEventSigningPrivateKey: 'test',
45
- },
46
- );
47
- };
48
16
  beforeEach(async () => {
49
17
  builder = createBuilder();
50
18
  });
@@ -38,12 +38,21 @@ export class RascalConfigBuilder {
38
38
  protected currentServiceId: string;
39
39
  protected destinationServiceId: string;
40
40
 
41
+ /**
42
+ * Service ID of an event to which the service subscripbes to.
43
+ * Used by the `cachePublicSigningKeys` to request only relevant public keys
44
+ * from the Key Service.
45
+ *
46
+ * Only set if `subscribeForEvent` is called on the builder.
47
+ */
48
+ public subscribedEventServiceId: string | undefined;
49
+
41
50
  /**
42
51
  * @param info an info object that contains general information about message type
43
52
  * @param config general RabbitMQ configuration object
44
53
  */
45
54
  constructor(
46
- protected readonly info: MessagingSettings,
55
+ public readonly info: MessagingSettings,
47
56
  protected readonly config: MessagingConfig,
48
57
  ) {
49
58
  this.currentServiceId = this.config.serviceId;
@@ -107,6 +116,7 @@ export class RascalConfigBuilder {
107
116
  public subscribeForEvent<TContent>(
108
117
  buildMessageHandler: (broker: BrokerProxy) => MessageHandler<TContent>,
109
118
  ): RascalConfigBuilder {
119
+ this.subscribedEventServiceId = this.info.serviceId;
110
120
  return this.subscribeForMessage('event', buildMessageHandler);
111
121
  }
112
122
 
@@ -0,0 +1,10 @@
1
+ query GetPublicSigningKeys($filter: PublicKeyFilter!) {
2
+ publicKeys(filter: $filter) {
3
+ nodes {
4
+ serviceId
5
+ key
6
+ version
7
+ isActive
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,115 @@
1
+ import { Logger, MosaicError } from '@axinom/mosaic-service-common';
2
+ import { GraphQLClient } from 'graphql-request';
3
+ import { getSdk } from '../generated/key-service';
4
+ import { RascalConfigBuilder } from '../rascal-config-builder';
5
+ import { GetEventSigningTokenFunc, PublicSigningKey } from '../types';
6
+ import { EventSigningErrors } from './event-signing-errors';
7
+ import {
8
+ getCachedSigningPublicKeys,
9
+ getCachedSigningToken,
10
+ setCachedSigningPublicKeys,
11
+ setCachedSigningToken,
12
+ } from './signing-cache';
13
+
14
+ /**
15
+ * Retrieves and caches a list of public keys for services which events the
16
+ * current service is expecting to receive.
17
+ *
18
+ * @param builders - Used to determine which events the service is expecting to
19
+ * receive. Passing a list of all builders used by `setupMessagingBroker` is recommended.
20
+ * @param keyServiceBaseUrl - Base URL of Mosaic Key Service
21
+ * @param getTokenCallback - Function to retrieve authorization token result to make a
22
+ * request to the Key Service
23
+ * @param logger - Optional instance of the Mosaic Logger. If not provided - new
24
+ * instance will be created and used by the function itself.
25
+ */
26
+ export const cachePublicSigningKeys = async (
27
+ builders: RascalConfigBuilder[],
28
+ keyServiceBaseUrl: string,
29
+ getTokenCallback: GetEventSigningTokenFunc,
30
+ logger?: Logger,
31
+ ): Promise<PublicSigningKey[]> => {
32
+ logger = logger ?? new Logger({ context: cachePublicSigningKeys.name });
33
+
34
+ let token = getCachedSigningToken();
35
+ if (!token) {
36
+ const { accessToken, expiresInSeconds } = await getTokenCallback();
37
+ setCachedSigningToken(accessToken, expiresInSeconds - 60);
38
+ token = accessToken;
39
+ }
40
+ const serviceIds = [
41
+ ...new Set(
42
+ builders
43
+ .map((x) => x.subscribedEventServiceId)
44
+ .filter((id): id is string => !!id),
45
+ ),
46
+ ];
47
+
48
+ const client = new GraphQLClient(new URL('graphql', keyServiceBaseUrl).href);
49
+ const { GetPublicSigningKeys } = getSdk(client);
50
+ const { data } = await GetPublicSigningKeys(
51
+ {
52
+ filter: {
53
+ serviceId: { in: serviceIds },
54
+ },
55
+ },
56
+ { Authorization: `Bearer ${token}` },
57
+ );
58
+ setCachedSigningPublicKeys(data.publicKeys?.nodes, 60 * 10);
59
+ logger.log({
60
+ message: 'Public signing keys successfully cached.',
61
+ details: { serviceIds, keys: data.publicKeys?.nodes },
62
+ });
63
+ return data.publicKeys?.nodes ?? [];
64
+ };
65
+
66
+ /**
67
+ * Retrieves a public key for specific service ID and version. If not available
68
+ * in the cache - cache is updated using `cachePublicSigningKeys` and cache is
69
+ * checked again. If even still there is no matching key - error is thrown,
70
+ * because at this point, if there is a need to validate the received event - it
71
+ * is expected for the service that produced said event to register its own
72
+ * public key with the Key Service.
73
+ *
74
+ *
75
+ * @param serviceId - ID of the service that signed event originated from
76
+ * @param version - Version of the public key of the service that signed event originated from
77
+ * @param builders - Used to determine which events the service is expecting to
78
+ * receive. Passing a list of all builders used by `setupMessagingBroker` is recommended.
79
+ * @param keyServiceBaseUrl - Base URL of Mosaic Key Service
80
+ * @param getTokenCallback - Function to retrieve authorization token result to make a
81
+ * request to the Key Service
82
+ * @param logger - Optional instance of the Mosaic Logger. If not provided - new
83
+ * instance will be created and used by the function itself.
84
+ */
85
+ export const getCachedPublicKey = async (
86
+ serviceId: string,
87
+ version: number,
88
+ builders: RascalConfigBuilder[],
89
+ keyServiceBaseUrl: string,
90
+ getTokenCallback: GetEventSigningTokenFunc,
91
+ logger?: Logger,
92
+ ): Promise<string> => {
93
+ let found = (getCachedSigningPublicKeys() ?? []).find(
94
+ (x) => x.serviceId === serviceId && x.version === version,
95
+ );
96
+
97
+ if (!found) {
98
+ found = (
99
+ await cachePublicSigningKeys(
100
+ builders,
101
+ keyServiceBaseUrl,
102
+ getTokenCallback,
103
+ logger,
104
+ )
105
+ ).find((x) => x.serviceId === serviceId && x.version === version);
106
+ }
107
+
108
+ if (!found) {
109
+ throw new MosaicError({
110
+ ...EventSigningErrors.SigningPublicKeyNotFound,
111
+ messageParams: [serviceId, version],
112
+ });
113
+ }
114
+ return found.key;
115
+ };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Possible errors that are explicitly handled during event signing and validation.
3
+ */
4
+ export const EventSigningErrors = {
5
+ SigningPublicKeyNotFound: {
6
+ message:
7
+ "Unable to find the public signing key for service with ID '%s' and version %s. Please contact Axinom Support.",
8
+ code: 'SIGNING_PUBLIC_KEY_NOT_FOUND',
9
+ },
10
+ SignatureValidationFailed: {
11
+ message:
12
+ 'Event signature validation has failed. The source of event message might be untrustworthy. Please contact Axinom Support.',
13
+ code: 'SIGNATURE_VALIDATION_FAILED',
14
+ },
15
+ KeyServiceNotAccessible: {
16
+ message:
17
+ 'The Key service is not accessible. Please contact Axinom support.',
18
+ code: 'KEY_SERVICE_NOT_ACCESSIBLE',
19
+ },
20
+ UnexpectedMessageType: {
21
+ message:
22
+ 'The received message has a type that the service does not expect. The source of message might be untrustworthy. Please contact Axinom Support.',
23
+ code: 'UNEXPECTED_MESSAGE_TYPE',
24
+ },
25
+ SigningHeadersMissing: {
26
+ message:
27
+ 'The received event message is missing either signature or version headers. The source of message might be untrustworthy. Please contact Axinom Support.',
28
+ code: 'SIGNING_HEADERS_MISSING',
29
+ },
30
+ ServiceIdNotFound: {
31
+ message:
32
+ 'The received message is missing a service ID at the start of the routing key or related message builder has no service ID. This is probably an implementation bug. Please contact the Service Support.',
33
+ code: 'SERVICE_ID_NOT_FOUND',
34
+ },
35
+ } as const;
@@ -0,0 +1,3 @@
1
+ export * from './cache-public-signing-keys';
2
+ export * from './event-signing-errors';
3
+ export * from './register-public-signing-key';
@@ -0,0 +1,10 @@
1
+ mutation RegisterPublicSigningKey($input: RegisterPublicKeyInput!) {
2
+ registerPublicKey(input: $input) {
3
+ publicKey {
4
+ version
5
+ serviceId
6
+ key
7
+ isActive
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,95 @@
1
+ /* eslint-disable no-console */
2
+ import { stub } from 'jest-auto-stub';
3
+ import 'jest-extended';
4
+ import * as keyService from '../generated/key-service';
5
+ import { GetEventSigningTokenFunc } from '../types';
6
+ import { registerPublicSigningKey } from './register-public-signing-key';
7
+ import {
8
+ exportedForTesting,
9
+ getCachedSigningPublicKeys,
10
+ getCachedSigningToken,
11
+ } from './signing-cache';
12
+
13
+ describe('registerPublicSigningKey', () => {
14
+ const keyServiceBaseUrl = 'http://localhost:12100/graphiql';
15
+ const managedServiceId = 'ax-test-service';
16
+ const validPublicKey =
17
+ 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr6hjEsxp5nIfgQTXfPcYSTMAAhYFLkygyCQHI4aIiTL1fddWs9nIBomiIsTjapggQGqRn7GdmztRSRaYuHS5ll6for6j5UbP5S6FKP9wRZomE952Uet+qIMOicvFgk/YB8OAHyOcZ8dZSYqMpR0BMRHxLjpj+FS3FPdiIpb5Dj10lXcUP0I8jatbpp9iqCFDQaY7hxSFE2p5XJKPYoA0/9nLZiRX5LJF5QsOx1eAZwT0qgS4Q5SkGnzYT4/V5/d4eLBofOU3DwZPdBnmf10NyyeTpn9ae6QXWv7kouYIOhcmZaxWCo4loJpAd4Iv+zL7JwijJqcNcLHr0SlaRmaUNQIDAQAB';
18
+
19
+ const getTokenCallback: GetEventSigningTokenFunc = async () => ({
20
+ accessToken: 'SGVsbG8gVGhlcmUh',
21
+ expiresInSeconds: 86400,
22
+ tokenType: 'ManagedServiceAccount',
23
+ });
24
+
25
+ let keyServiceSpy: jest.SpyInstance;
26
+ let consoleSpy: jest.SpyInstance;
27
+ let registerKeyResponse = () => ({});
28
+
29
+ beforeAll(() => {
30
+ keyServiceSpy = jest.spyOn(keyService, 'getSdk').mockImplementation(() =>
31
+ stub<keyService.Sdk>({
32
+ RegisterPublicSigningKey: () => ({ data: registerKeyResponse() }),
33
+ }),
34
+ );
35
+ consoleSpy = jest
36
+ .spyOn(console, 'log')
37
+ .mockImplementation((obj) => JSON.parse(obj));
38
+ });
39
+
40
+ afterEach(() => {
41
+ jest.clearAllMocks();
42
+ registerKeyResponse = () => ({});
43
+ exportedForTesting.clearSigningCache();
44
+ });
45
+
46
+ it('register public key successfully -> success log', async () => {
47
+ // Arrange
48
+ const config = {
49
+ rmqEventSigningKeyVersion: 1,
50
+ serviceId: managedServiceId,
51
+ rmqEventSigningPublicKey: validPublicKey,
52
+ rmqEventSigningKeyVersionsToRevoke: [],
53
+ };
54
+ const response = {
55
+ key: config.rmqEventSigningPublicKey,
56
+ serviceId: config.serviceId,
57
+ version: config.rmqEventSigningKeyVersion,
58
+ isActive: true,
59
+ };
60
+ registerKeyResponse = () => ({
61
+ registerPublicKey: {
62
+ publicKey: response,
63
+ },
64
+ });
65
+
66
+ // Act
67
+ await registerPublicSigningKey(config, keyServiceBaseUrl, getTokenCallback);
68
+
69
+ // Assert
70
+ expect(keyServiceSpy).toHaveBeenCalledTimes(1);
71
+ expect(consoleSpy).toHaveBeenCalledTimes(1);
72
+ expect(consoleSpy.mock.results).toMatchObject([
73
+ {
74
+ type: 'return',
75
+ value: {
76
+ context: 'registerPublicSigningKey',
77
+ details: {
78
+ key: validPublicKey,
79
+ serviceId: managedServiceId,
80
+ version: 1,
81
+ versionsToRevoke: [],
82
+ },
83
+ loglevel: 'INFO',
84
+ message: 'Public signing key successfully registered.',
85
+ },
86
+ },
87
+ ]);
88
+
89
+ const cachedToken = getCachedSigningToken();
90
+ expect(cachedToken).toEqual((await getTokenCallback()).accessToken);
91
+
92
+ const cachedKeys = getCachedSigningPublicKeys();
93
+ expect(cachedKeys).toEqual([response]);
94
+ });
95
+ });
@@ -0,0 +1,59 @@
1
+ import { Logger } from '@axinom/mosaic-service-common';
2
+ import { GraphQLClient } from 'graphql-request';
3
+ import { RegisterPublicKeyInput, getSdk } from '../generated/key-service';
4
+ import { GetEventSigningTokenFunc, SigningRegistrationConfig } from '../types';
5
+ import {
6
+ getCachedSigningPublicKeys,
7
+ setCachedSigningPublicKeys,
8
+ setCachedSigningToken,
9
+ } from './signing-cache';
10
+
11
+ /**
12
+ * Registers a service public key with the Key service. Caches the used token
13
+ * and the single registered public key. If some public keys were already cached - newly registered public key is added to the cached list.
14
+ *
15
+ * @param config - service configuration object containing all values relavant
16
+ * for event signing
17
+ * @param keyServiceBaseUrl - Base URL of Mosaic Key Service
18
+ * @param getTokenCallback - Function to retrieve authorization token result to make a
19
+ * registration request to the Key Service
20
+ * @param logger - Optional instance of the Mosaic Logger. If not provided - new
21
+ * instance will be created and used by the function itself.
22
+ */
23
+ export const registerPublicSigningKey = async (
24
+ config: SigningRegistrationConfig,
25
+ keyServiceBaseUrl: string,
26
+ getTokenCallback: GetEventSigningTokenFunc,
27
+ logger?: Logger,
28
+ ): Promise<void> => {
29
+ logger = logger ?? new Logger({ context: registerPublicSigningKey.name });
30
+
31
+ const input: RegisterPublicKeyInput = {
32
+ version: config.rmqEventSigningKeyVersion,
33
+ versionsToRevoke: config.rmqEventSigningKeyVersionsToRevoke,
34
+ serviceId: config.serviceId,
35
+ key: config.rmqEventSigningPublicKey,
36
+ };
37
+ const { accessToken, expiresInSeconds } = await getTokenCallback();
38
+ setCachedSigningToken(accessToken, expiresInSeconds - 60);
39
+
40
+ const client = new GraphQLClient(new URL('graphql', keyServiceBaseUrl).href);
41
+ const { RegisterPublicSigningKey } = getSdk(client);
42
+ const { data } = await RegisterPublicSigningKey(
43
+ { input },
44
+ { Authorization: `Bearer ${accessToken}` },
45
+ );
46
+
47
+ if (data.registerPublicKey?.publicKey) {
48
+ const currentKeys = getCachedSigningPublicKeys();
49
+ setCachedSigningPublicKeys(
50
+ [...(currentKeys ?? []), data.registerPublicKey.publicKey],
51
+ 60 * 10,
52
+ );
53
+ }
54
+
55
+ logger.log({
56
+ message: 'Public signing key successfully registered.',
57
+ details: { ...input },
58
+ });
59
+ };
@@ -0,0 +1,49 @@
1
+ import NodeCache from 'node-cache';
2
+ import { PublicSigningKey } from '../types';
3
+ const signingCache = new NodeCache({ stdTTL: 60 * 10 }); // cache for 10 minutes
4
+ const signingCacheKeys = 'keys';
5
+ const signingCacheToken = 'token';
6
+
7
+ /**
8
+ * Retrieves the cached authorization token for requests related to event signing.
9
+ */
10
+ export const getCachedSigningToken = (): string | undefined => {
11
+ return signingCache.get<string>(signingCacheToken);
12
+ };
13
+
14
+ /**
15
+ * Caches the authorization token for requests related to event signing.
16
+ */
17
+ export const setCachedSigningToken = (
18
+ token: string,
19
+ expiration: number,
20
+ ): void => {
21
+ signingCache.set(signingCacheToken, token, expiration);
22
+ };
23
+
24
+ /**
25
+ * Retrieves the cached array of event signing public keys.
26
+ */
27
+ export const getCachedSigningPublicKeys = ():
28
+ | PublicSigningKey[]
29
+ | undefined => {
30
+ return signingCache.get<PublicSigningKey[]>(signingCacheKeys);
31
+ };
32
+
33
+ /**
34
+ * Caches the array of event signing public keys.
35
+ */
36
+ export const setCachedSigningPublicKeys = (
37
+ keys: PublicSigningKey[] | undefined,
38
+ expiration: number,
39
+ ): void => {
40
+ signingCache.set(signingCacheKeys, keys, expiration);
41
+ };
42
+
43
+ /**
44
+ * Only used for unit tests. Helps clear cache so tests would not interfere with
45
+ * one another.
46
+ */
47
+ export const exportedForTesting = {
48
+ clearSigningCache: (): void => signingCache.flushAll(),
49
+ };
@@ -0,0 +1,34 @@
1
+ import { MessagingSettings } from '@axinom/mosaic-message-bus-abstractions';
2
+ import { RascalConfigBuilder } from '../../rascal-config-builder';
3
+
4
+ export const createBuilder = (
5
+ infoOverrides: Partial<MessagingSettings> = {},
6
+ ): RascalConfigBuilder => {
7
+ return new RascalConfigBuilder(
8
+ {
9
+ action: 'event',
10
+ aggregateType: 'test',
11
+ messageType: 'TestKey',
12
+ queue: 'test-queue',
13
+ routingKey: 'test.key',
14
+ ...infoOverrides,
15
+ },
16
+ {
17
+ isDev: false,
18
+ serviceId: 'test-service-id',
19
+ rmqProtocol: 'test-protocol',
20
+ rmqVHost: 'test-vhost',
21
+ rmqHost: 'test-host',
22
+ rmqPort: 123,
23
+ rmqUser: 'test-user',
24
+ rmqPassword: 'test-pw',
25
+ rmqVHostAssert: true,
26
+ rmqChannelMax: 5,
27
+ rmqMgmtHost: 'test',
28
+ rmqMgmtPort: 1234,
29
+ rmqMgmtProtocol: 'amqp',
30
+ rmqEventSigningKeyVersion: 1,
31
+ rmqEventSigningPrivateKey: 'test',
32
+ },
33
+ );
34
+ };
@@ -0,0 +1 @@
1
+ export * from './create-builder';
@@ -3,4 +3,4 @@ export * from './message-envelope';
3
3
  export * from './message-envelope-overrides';
4
4
  export * from './message-info';
5
5
  export * from './on-message-middleware';
6
- export * from './signing-details';
6
+ export * from './signing';