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