Skip to content

Commit cfd4965

Browse files
committed
Implement signature validation
1 parent b3d2bd1 commit cfd4965

9 files changed

Lines changed: 467 additions & 6 deletions

File tree

services-custom/sns-message-manager/pom.xml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
<parent>
2222
<groupId>software.amazon.awssdk</groupId>
2323
<artifactId>aws-sdk-java-pom</artifactId>
24-
<version>2.42.3-SNAPSHOT</version>
24+
<version>2.42.15-SNAPSHOT</version>
2525
<relativePath>../../pom.xml</relativePath>
2626
</parent>
2727
<artifactId>sns-message-manager</artifactId>
@@ -68,6 +68,11 @@
6868
<artifactId>sdk-core</artifactId>
6969
<version>${project.version}</version>
7070
</dependency>
71+
<dependency>
72+
<groupId>org.junit.jupiter</groupId>
73+
<artifactId>junit-jupiter</artifactId>
74+
<scope>test</scope>
75+
</dependency>
7176
<dependency>
7277
<groupId>software.amazon.awssdk</groupId>
7378
<artifactId>http-client-spi</artifactId>
@@ -78,11 +83,6 @@
7883
<artifactId>httpclient5</artifactId>
7984
<version>${httpcomponents.client5.version}</version>
8085
</dependency>
81-
<dependency>
82-
<groupId>org.junit.jupiter</groupId>
83-
<artifactId>junit-jupiter-engine</artifactId>
84-
<scope>test</scope>
85-
</dependency>
8686
<dependency>
8787
<groupId>org.assertj</groupId>
8888
<artifactId>assertj-core</artifactId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.messagemanager.sns.internal;
17+
18+
import java.nio.charset.StandardCharsets;
19+
import java.security.InvalidKeyException;
20+
import java.security.NoSuchAlgorithmException;
21+
import java.security.PublicKey;
22+
import java.security.Signature;
23+
import java.security.SignatureException;
24+
import java.util.StringJoiner;
25+
import software.amazon.awssdk.annotations.SdkInternalApi;
26+
import software.amazon.awssdk.core.SdkBytes;
27+
import software.amazon.awssdk.core.exception.SdkClientException;
28+
import software.amazon.awssdk.messagemanager.sns.model.SignatureVersion;
29+
import software.amazon.awssdk.messagemanager.sns.model.SnsMessage;
30+
import software.amazon.awssdk.messagemanager.sns.model.SnsNotification;
31+
import software.amazon.awssdk.messagemanager.sns.model.SnsSubscriptionConfirmation;
32+
import software.amazon.awssdk.messagemanager.sns.model.SnsUnsubscribeConfirmation;
33+
import software.amazon.awssdk.utils.Logger;
34+
import software.amazon.awssdk.utils.Validate;
35+
36+
/**
37+
* See
38+
* <a href="https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message-verify-message-signature.html">
39+
* The official documentation.</a>
40+
*/
41+
@SdkInternalApi
42+
public final class SignatureValidator {
43+
private static final Logger LOG = Logger.loggerFor(SignatureValidator.class);
44+
45+
private static final String MESSAGE = "Message";
46+
private static final String MESSAGE_ID = "MessageId";
47+
private static final String SUBJECT = "Subject";
48+
private static final String SUBSCRIBE_URL = "SubscribeURL";
49+
private static final String TIMESTAMP = "Timestamp";
50+
private static final String TOKEN = "Token";
51+
private static final String TOPIC_ARN = "TopicArn";
52+
private static final String TYPE = "Type";
53+
54+
private static final String NEWLINE = "\n";
55+
56+
public void validateSignature(SnsMessage message, PublicKey publicKey) {
57+
Validate.paramNotNull(message, "message");
58+
Validate.paramNotNull(publicKey, "publicKey");
59+
60+
SdkBytes messageSignature = message.signature();
61+
if (messageSignature == null) {
62+
throw SdkClientException.create("Message signature cannot be null");
63+
}
64+
65+
SignatureVersion signatureVersion = message.signatureVersion();
66+
if (signatureVersion == null) {
67+
throw SdkClientException.create("Message signature version cannot be null");
68+
}
69+
70+
if (message.timestamp() == null) {
71+
throw SdkClientException.create("Message timestamp cannot be null");
72+
}
73+
74+
String canonicalMessage = buildCanonicalMessage(message);
75+
LOG.debug(() -> String.format("Canonical message: %s%n", canonicalMessage));
76+
77+
Signature signature = getSignature(signatureVersion);
78+
79+
verifySignature(canonicalMessage, messageSignature, publicKey, signature);
80+
}
81+
82+
private static String buildCanonicalMessage(SnsMessage message) {
83+
switch (message.type()) {
84+
case NOTIFICATION:
85+
return buildCanonicalMessage((SnsNotification) message);
86+
case SUBSCRIPTION_CONFIRMATION:
87+
return buildCanonicalMessage((SnsSubscriptionConfirmation) message);
88+
case UNSUBSCRIBE_CONFIRMATION:
89+
return buildCanonicalMessage((SnsUnsubscribeConfirmation) message);
90+
default:
91+
throw new IllegalStateException(String.format("Unsupported SNS message type: %s", message.type()));
92+
}
93+
}
94+
95+
private static String buildCanonicalMessage(SnsNotification notification) {
96+
StringJoiner joiner = new StringJoiner(NEWLINE, "", NEWLINE);
97+
joiner.add(MESSAGE).add(notification.message());
98+
joiner.add(MESSAGE_ID).add(notification.messageId());
99+
100+
if (notification.subject() != null) {
101+
joiner.add(SUBJECT).add(notification.subject());
102+
}
103+
104+
joiner.add(TIMESTAMP).add(notification.timestamp().toString());
105+
joiner.add(TOPIC_ARN).add(notification.topicArn());
106+
joiner.add(TYPE).add(notification.type().toString());
107+
108+
return joiner.toString();
109+
}
110+
111+
// Message, MessageId, SubscribeURL, Timestamp, Token, TopicArn, and Type.
112+
private static String buildCanonicalMessage(SnsSubscriptionConfirmation message) {
113+
StringJoiner joiner = new StringJoiner(NEWLINE, "", NEWLINE);
114+
joiner.add(MESSAGE).add(message.message());
115+
joiner.add(MESSAGE_ID).add(message.messageId());
116+
joiner.add(SUBSCRIBE_URL).add(message.subscribeUrl().toString());
117+
joiner.add(TIMESTAMP).add(message.timestamp().toString());
118+
joiner.add(TOKEN).add(message.token());
119+
joiner.add(TOPIC_ARN).add(message.topicArn());
120+
joiner.add(TYPE).add(message.type().toString());
121+
122+
return joiner.toString();
123+
}
124+
125+
private static String buildCanonicalMessage(SnsUnsubscribeConfirmation message) {
126+
StringJoiner joiner = new StringJoiner(NEWLINE, "", NEWLINE);
127+
joiner.add(MESSAGE).add(message.message());
128+
joiner.add(MESSAGE_ID).add(message.messageId());
129+
joiner.add(SUBSCRIBE_URL).add(message.subscribeUrl().toString());
130+
joiner.add(TIMESTAMP).add(message.timestamp().toString());
131+
joiner.add(TOKEN).add(message.token());
132+
joiner.add(TOPIC_ARN).add(message.topicArn());
133+
joiner.add(TYPE).add(message.type().toString());
134+
135+
return joiner.toString();
136+
}
137+
138+
private static void verifySignature(String canonicalMessage, SdkBytes messageSignature, PublicKey publicKey,
139+
Signature signature) {
140+
141+
try {
142+
signature.initVerify(publicKey);
143+
signature.update(canonicalMessage.getBytes(StandardCharsets.UTF_8));
144+
145+
boolean isValid = signature.verify(messageSignature.asByteArray());
146+
147+
if (!isValid) {
148+
throw SdkClientException.create("The computed signature did not match the expected signature");
149+
}
150+
} catch (InvalidKeyException e) {
151+
throw SdkClientException.create("The public key is invalid", e);
152+
} catch (SignatureException e) {
153+
throw SdkClientException.create("The signature is invalid", e);
154+
}
155+
}
156+
157+
private static Signature getSignature(SignatureVersion signatureVersion) {
158+
try {
159+
switch (signatureVersion) {
160+
case VERSION_1:
161+
return Signature.getInstance("SHA1withRSA");
162+
case VERSION_2:
163+
return Signature.getInstance("SHA256withRSA");
164+
default:
165+
throw new IllegalArgumentException("Unsupported signature version: " + signatureVersion);
166+
}
167+
} catch (NoSuchAlgorithmException e) {
168+
throw new RuntimeException("Unable to create Signature for " + signatureVersion, e);
169+
}
170+
}
171+
}

0 commit comments

Comments
 (0)