@axinom/mosaic-message-bus 0.32.0-rc.8 → 0.32.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.
- package/dist/broker.js +5 -2
- package/dist/broker.js.map +1 -1
- package/dist/common/constants.d.ts +27 -0
- package/dist/common/constants.d.ts.map +1 -1
- package/dist/common/constants.js +28 -1
- package/dist/common/constants.js.map +1 -1
- package/dist/generated/key-service.d.ts +438 -0
- package/dist/generated/key-service.d.ts.map +1 -0
- package/dist/generated/key-service.js +162 -0
- package/dist/generated/key-service.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/middleware/index.d.ts +1 -0
- package/dist/middleware/index.d.ts.map +1 -1
- package/dist/middleware/index.js +1 -0
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/validate-signed-event-middleware.d.ts +36 -0
- package/dist/middleware/validate-signed-event-middleware.d.ts.map +1 -0
- package/dist/middleware/validate-signed-event-middleware.js +57 -0
- package/dist/middleware/validate-signed-event-middleware.js.map +1 -0
- package/dist/publication.d.ts.map +1 -1
- package/dist/publication.js +3 -2
- package/dist/publication.js.map +1 -1
- package/dist/rascal-config-builder.d.ts +9 -1
- package/dist/rascal-config-builder.d.ts.map +1 -1
- package/dist/rascal-config-builder.js +1 -0
- package/dist/rascal-config-builder.js.map +1 -1
- package/dist/signing/cache-public-signing-keys.d.ts +37 -0
- package/dist/signing/cache-public-signing-keys.d.ts.map +1 -0
- package/dist/signing/cache-public-signing-keys.js +81 -0
- package/dist/signing/cache-public-signing-keys.js.map +1 -0
- package/dist/signing/event-signing-errors.d.ts +30 -0
- package/dist/signing/event-signing-errors.d.ts.map +1 -0
- package/dist/signing/event-signing-errors.js +33 -0
- package/dist/signing/event-signing-errors.js.map +1 -0
- package/dist/signing/index.d.ts +4 -0
- package/dist/signing/index.d.ts.map +1 -0
- package/dist/signing/index.js +20 -0
- package/dist/signing/index.js.map +1 -0
- package/dist/signing/register-public-signing-key.d.ts +16 -0
- package/dist/signing/register-public-signing-key.d.ts.map +1 -0
- package/dist/signing/register-public-signing-key.js +44 -0
- package/dist/signing/register-public-signing-key.js.map +1 -0
- package/dist/signing/signing-cache.d.ts +25 -0
- package/dist/signing/signing-cache.d.ts.map +1 -0
- package/dist/signing/signing-cache.js +46 -0
- package/dist/signing/signing-cache.js.map +1 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/signing.d.ts +30 -0
- package/dist/types/signing.d.ts.map +1 -0
- package/dist/types/{signing-details.js → signing.js} +1 -1
- package/dist/types/signing.js.map +1 -0
- package/package.json +16 -5
- package/src/common/constants.ts +32 -0
- package/src/generated/key-service.ts +459 -0
- package/src/index.ts +1 -0
- package/src/middleware/index.ts +1 -0
- package/src/middleware/validate-signed-event-middleware.spec.ts +679 -0
- package/src/middleware/validate-signed-event-middleware.ts +136 -0
- package/src/publication.ts +9 -5
- package/src/rascal-config-builder.spec.ts +1 -33
- package/src/rascal-config-builder.ts +11 -1
- package/src/signing/cache-public-signing-keys.graphql +10 -0
- package/src/signing/cache-public-signing-keys.ts +115 -0
- package/src/signing/event-signing-errors.ts +35 -0
- package/src/signing/index.ts +3 -0
- package/src/signing/register-public-signing-key.graphql +10 -0
- package/src/signing/register-public-signing-key.spec.ts +95 -0
- package/src/signing/register-public-signing-key.ts +59 -0
- package/src/signing/signing-cache.ts +49 -0
- package/src/tests/utils/create-builder.ts +34 -0
- package/src/tests/utils/index.ts +1 -0
- package/src/types/index.ts +1 -1
- package/src/types/signing.ts +50 -0
- package/dist/types/signing-details.d.ts +0 -3
- package/dist/types/signing-details.d.ts.map +0 -1
- package/dist/types/signing-details.js.map +0 -1
- 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
|
+
};
|
package/src/publication.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
[
|
|
85
|
-
[
|
|
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
|
-
[
|
|
100
|
-
[
|
|
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
|
-
|
|
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,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,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';
|
package/src/types/index.ts
CHANGED