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 static org .assertj .core .api .Assertions .assertThatThrownBy ;
19+ import static org .mockito .Mockito .mock ;
20+ import static org .mockito .Mockito .when ;
21+
22+ import java .io .InputStream ;
23+ import java .net .URI ;
24+ import java .security .PublicKey ;
25+ import java .security .cert .CertificateException ;
26+ import java .security .cert .CertificateFactory ;
27+ import java .security .cert .X509Certificate ;
28+ import java .time .Instant ;
29+ import java .util .List ;
30+ import java .util .stream .Collectors ;
31+ import java .util .stream .Stream ;
32+ import org .junit .jupiter .api .BeforeAll ;
33+ import org .junit .jupiter .api .Test ;
34+ import org .junit .jupiter .params .ParameterizedTest ;
35+ import org .junit .jupiter .params .provider .MethodSource ;
36+ import software .amazon .awssdk .core .SdkBytes ;
37+ import software .amazon .awssdk .core .exception .SdkClientException ;
38+ import software .amazon .awssdk .messagemanager .sns .model .SignatureVersion ;
39+ import software .amazon .awssdk .messagemanager .sns .model .SnsMessage ;
40+ import software .amazon .awssdk .messagemanager .sns .model .SnsNotification ;
41+
42+ class SignatureValidatorTest {
43+ private static final String RESOURCE_ROOT = "/software/amazon/awssdk/messagemanager/sns/internal/" ;
44+ private static final String SIGNING_CERT_RESOURCE = "SimpleNotificationService-7506a1e35b36ef5a444dd1a8e7cc3ed8.pem" ;
45+ private static final SignatureValidator VALIDATOR = new SignatureValidator ();
46+ private static X509Certificate signingCertificate ;
47+
48+ @ BeforeAll
49+ static void setup () throws CertificateException {
50+ InputStream is = resourceAsStream (SIGNING_CERT_RESOURCE );
51+ CertificateFactory factory = CertificateFactory .getInstance ("X.509" );
52+ signingCertificate = (X509Certificate ) factory .generateCertificate (is );
53+ }
54+
55+ @ ParameterizedTest (name = "{0}" )
56+ @ MethodSource ("validMessages" )
57+ void validateSignature_signatureValid_doesNotThrow (TestCase tc ) {
58+ SnsMessageUnmarshaller unmarshaller = new SnsMessageUnmarshaller ();
59+ SnsMessage msg = unmarshaller .unmarshall (resourceAsStream (tc .messageJsonResource ));
60+ VALIDATOR .validateSignature (msg , signingCertificate .getPublicKey ());
61+ }
62+
63+ @ Test
64+ void validateSignature_signatureMismatch_throws () {
65+ SnsNotification notification = SnsNotification .builder ()
66+ .message ("hello world" )
67+ .messageId ("message-id" )
68+ .signature (SdkBytes .fromByteArray (new byte [256 ]))
69+ .signatureVersion (SignatureVersion .VERSION_1 )
70+ .build ();
71+
72+ assertThatThrownBy (() -> VALIDATOR .validateSignature (notification , signingCertificate .getPublicKey ()))
73+ .isInstanceOf (SdkClientException .class )
74+ .hasMessageContaining ("The computed signature did not match the expected signature" );
75+ }
76+
77+ @ Test
78+ void validateSignature_signatureMissing_throws () {
79+ SnsNotification notification = SnsNotification .builder ()
80+ .subject ("hello world" )
81+ .message ("hello world" )
82+ .messageId ("message-id" )
83+ .timestamp (Instant .now ())
84+ .unsubscribeUrl (URI .create ("https://my-test-service.amazonaws.com" ))
85+ .signingCertUrl (URI .create ("https://my-test-service.amazonaws.com/cert"
86+ + ".pem" ))
87+ .signatureVersion (SignatureVersion .VERSION_1 )
88+ .build ();
89+
90+ assertThatThrownBy (() -> VALIDATOR .validateSignature (notification , signingCertificate .getPublicKey ()))
91+ .isInstanceOf (SdkClientException .class )
92+ .hasMessage ("Message signature cannot be null" );
93+ }
94+
95+ @ Test
96+ void validateSignature_signatureVersionMissing_throws () {
97+ SnsNotification notification = SnsNotification .builder ()
98+ .subject ("hello world" )
99+ .message ("hello world" )
100+ .messageId ("message-id" )
101+ .signature (SdkBytes .fromByteArray (new byte [256 ]))
102+ .timestamp (Instant .now ())
103+ .unsubscribeUrl (URI .create ("https://my-test-service.amazonaws.com" ))
104+ .signingCertUrl (URI .create ("https://my-test-service.amazonaws.com/cert"
105+ + ".pem" ))
106+ .build ();
107+
108+
109+ assertThatThrownBy (() -> VALIDATOR .validateSignature (notification , signingCertificate .getPublicKey ()))
110+ .isInstanceOf (SdkClientException .class )
111+ .hasMessage ("Message signature version cannot be null" );
112+ }
113+
114+ @ Test
115+ void validateSignature_certInvalid_throws () throws CertificateException {
116+ SnsNotification notification = SnsNotification .builder ()
117+ .signature (SdkBytes .fromByteArray (new byte [1 ]))
118+ .signatureVersion (SignatureVersion .VERSION_1 )
119+ .build ();
120+
121+ PublicKey badKey = mock (PublicKey .class );
122+ when (badKey .getFormat ()).thenReturn ("X.509" );
123+ when (badKey .getAlgorithm ()).thenReturn ("RSA" );
124+ when (badKey .getEncoded ()).thenReturn (new byte [1 ]);
125+
126+ assertThatThrownBy (() -> VALIDATOR .validateSignature (notification , badKey ))
127+ .isInstanceOf (SdkClientException .class )
128+ .hasMessage ("The public key is invalid" );
129+ }
130+
131+ @ Test
132+ void validateSignature_signatureInvalid_throws () throws CertificateException {
133+ SnsNotification notification = SnsNotification .builder ()
134+ .subject ("hello world" )
135+ .message ("hello world" )
136+ .messageId ("message-id" )
137+ .signature (SdkBytes .fromByteArray (new byte [1 ]))
138+ .signatureVersion (SignatureVersion .VERSION_1 )
139+ .timestamp (Instant .now ())
140+ .unsubscribeUrl (URI .create ("https://my-test-service.amazonaws.com" ))
141+ .signingCertUrl (URI .create ("https://my-test-service.amazonaws.com/cert"
142+ + ".pem" ))
143+ .build ();
144+
145+ assertThatThrownBy (() -> VALIDATOR .validateSignature (notification , signingCertificate .getPublicKey ()))
146+ .isInstanceOf (SdkClientException .class )
147+ .hasMessage ("The signature is invalid" );
148+ }
149+
150+ private static List <TestCase > validMessages () {
151+ return Stream .of (
152+ new TestCase ("Notification - No Subject" , "test-notification-no-subject.json" ),
153+ new TestCase ("Notification - Version 2 signature" , "test-notification-signature-v2.json" ),
154+ new TestCase ("Notification with subject" , "test-notification-with-subject.json" ),
155+ new TestCase ("Subscription confirmation" , "test-subscription-confirmation.json" ),
156+ new TestCase ("Unsubscribe confirmation" , "test-unsubscribe-confirmation.json" )
157+ )
158+ .collect (Collectors .toList ());
159+ }
160+
161+ private static InputStream resourceAsStream (String resourceName ) {
162+ return SignatureValidatorTest .class .getResourceAsStream (RESOURCE_ROOT + resourceName );
163+ }
164+
165+ private static class TestCase {
166+ private String desription ;
167+ private String messageJsonResource ;
168+
169+ public TestCase (String desription , String messageJsonResource ) {
170+ this .desription = desription ;
171+ this .messageJsonResource = messageJsonResource ;
172+ }
173+
174+ @ Override
175+ public String toString () {
176+ return desription ;
177+ }
178+ }
179+ }
0 commit comments