Skip to content

Commit 7949e16

Browse files
committed
test: cover message signing fail-open compliance
1 parent f136659 commit 7949e16

1 file changed

Lines changed: 148 additions & 11 deletions

File tree

approov-service/src/test/java/io/approov/service/retrofit/ApproovDefaultMessageSigningContractTest.java

Lines changed: 148 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44
import static org.junit.Assert.assertFalse;
55
import static org.junit.Assert.assertNull;
66
import static org.junit.Assert.assertSame;
7+
import static org.junit.Assert.assertThrows;
78
import static org.junit.Assert.assertTrue;
89

10+
import io.approov.util.sig.ComponentProvider;
11+
import io.approov.util.sig.SignatureParameters;
12+
913
import java.nio.charset.StandardCharsets;
1014
import java.util.Base64;
1115

@@ -47,6 +51,20 @@ protected byte[] decodeBase64(String base64) {
4751
}
4852
}
4953

54+
private static final class UnsupportedAlgorithmFactory
55+
extends ApproovDefaultMessageSigning.SignatureParametersFactory {
56+
@Override
57+
protected SignatureParameters buildSignatureParameters(
58+
ApproovDefaultMessageSigning.OkHttpComponentProvider provider,
59+
ApproovRequestMutations changes) {
60+
return new SignatureParameters()
61+
.addComponentIdentifier(ComponentProvider.DC_METHOD)
62+
.addComponentIdentifier(ComponentProvider.DC_TARGET_URI)
63+
.addComponentIdentifier(changes.getTokenHeaderKey())
64+
.setAlg("unsupported-alg");
65+
}
66+
}
67+
5068
private static String derEncodedInstallSignature() {
5169
byte[] der = new byte[] { 0x30, 0x06, 0x02, 0x01, 0x01, 0x02, 0x01, 0x02 };
5270
return Base64.getEncoder().encodeToString(der);
@@ -67,13 +85,42 @@ private static Request signedRequestFixture() {
6785
.build();
6886
}
6987

88+
private static Request unsignedRequestFixture() {
89+
return signedRequestFixture().newBuilder()
90+
.removeHeader("Content-Digest")
91+
.removeHeader("Signature")
92+
.removeHeader("Signature-Input")
93+
.removeHeader("Signature-Base-Digest")
94+
.build();
95+
}
96+
97+
private static Request requestWithoutBodyFixture() {
98+
return new Request.Builder()
99+
.url("https://api.example.com/reply")
100+
.get()
101+
.header("Approov-Token", "Bearer jwt-token")
102+
.header("Approov-TraceID", "trace-123")
103+
.header("Authorization", "Bearer auth-token")
104+
.build();
105+
}
106+
70107
private static ApproovRequestMutations defaultChanges() {
71108
ApproovRequestMutations changes = new ApproovRequestMutations();
72109
changes.setTokenHeaderKey("Approov-Token");
73110
changes.setTraceIDHeaderKey("Approov-TraceID");
74111
return changes;
75112
}
76113

114+
private static void assertUnsignedWithoutSignatureHeaders(Request original, Request processed) {
115+
assertSame(original, processed);
116+
assertEquals("Bearer jwt-token", processed.header("Approov-Token"));
117+
assertEquals("trace-123", processed.header("Approov-TraceID"));
118+
assertNull(processed.header("Content-Digest"));
119+
assertNull(processed.header("Signature"));
120+
assertNull(processed.header("Signature-Input"));
121+
assertNull(processed.header("Signature-Base-Digest"));
122+
}
123+
77124
@Test
78125
public void defaultSigningAddsDigestAndSignatureHeaders() throws Exception {
79126
RecordingSigner signer = new RecordingSigner();
@@ -137,20 +184,11 @@ public void signingSkipsGracefullyWhenInstallSigningIsUnavailable() throws Excep
137184
RecordingSigner signer = new RecordingSigner();
138185
signer.setDefaultFactory(ApproovDefaultMessageSigning.generateDefaultSignatureParametersFactory());
139186
signer.installError = new ApproovException("no device signature available");
140-
Request request = signedRequestFixture().newBuilder()
141-
.removeHeader("Content-Digest")
142-
.removeHeader("Signature")
143-
.removeHeader("Signature-Input")
144-
.removeHeader("Signature-Base-Digest")
145-
.build();
187+
Request request = unsignedRequestFixture();
146188

147189
Request signed = signer.processedRequest(request, defaultChanges());
148190

149-
assertSame(request, signed);
150-
assertNull(signed.header("Content-Digest"));
151-
assertNull(signed.header("Signature"));
152-
assertNull(signed.header("Signature-Input"));
153-
assertNull(signed.header("Signature-Base-Digest"));
191+
assertUnsignedWithoutSignatureHeaders(request, signed);
154192
}
155193

156194
@Test
@@ -167,4 +205,103 @@ public void accountSigningUsesTheAccountSignatureFlow() throws Exception {
167205
assertNull(signer.lastInstallMessage);
168206
assertTrue(signer.lastAccountMessage.contains("\"approov-token\""));
169207
}
208+
209+
@Test
210+
public void accountSigningSkipsGracefullyWhenSignatureBase64IsInvalid() throws Exception {
211+
RecordingSigner signer = new RecordingSigner();
212+
signer.setDefaultFactory(ApproovDefaultMessageSigning.generateDefaultSignatureParametersFactory()
213+
.setUseAccountMessageSigning());
214+
signer.accountSignatureBase64 = "not-base64";
215+
Request request = unsignedRequestFixture();
216+
217+
Request signed = signer.processedRequest(request, defaultChanges());
218+
219+
assertTrue(signer.lastAccountMessage.contains("\"approov-token\""));
220+
assertUnsignedWithoutSignatureHeaders(request, signed);
221+
}
222+
223+
@Test
224+
public void installSigningSkipsGracefullyWhenSignatureBase64IsInvalid() throws Exception {
225+
RecordingSigner signer = new RecordingSigner();
226+
signer.setDefaultFactory(ApproovDefaultMessageSigning.generateDefaultSignatureParametersFactory());
227+
signer.installSignatureBase64 = "not-base64";
228+
Request request = unsignedRequestFixture();
229+
230+
Request signed = signer.processedRequest(request, defaultChanges());
231+
232+
assertTrue(signer.lastInstallMessage.contains("\"approov-token\""));
233+
assertUnsignedWithoutSignatureHeaders(request, signed);
234+
}
235+
236+
@Test
237+
public void installSigningSkipsGracefullyWhenDerSignatureIsMalformed() throws Exception {
238+
RecordingSigner signer = new RecordingSigner();
239+
signer.setDefaultFactory(ApproovDefaultMessageSigning.generateDefaultSignatureParametersFactory());
240+
signer.installSignatureBase64 = Base64.getEncoder().encodeToString(
241+
new byte[] { 0x31, 0x00 });
242+
Request request = unsignedRequestFixture();
243+
244+
Request signed = signer.processedRequest(request, defaultChanges());
245+
246+
assertTrue(signer.lastInstallMessage.contains("\"approov-token\""));
247+
assertUnsignedWithoutSignatureHeaders(request, signed);
248+
}
249+
250+
@Test
251+
public void installSigningSkipsGracefullyWhenDerSignatureIsTruncated() throws Exception {
252+
RecordingSigner signer = new RecordingSigner();
253+
signer.setDefaultFactory(ApproovDefaultMessageSigning.generateDefaultSignatureParametersFactory());
254+
signer.installSignatureBase64 = Base64.getEncoder().encodeToString(
255+
new byte[] { 0x30, 0x06, 0x02 });
256+
Request request = unsignedRequestFixture();
257+
258+
Request signed = signer.processedRequest(request, defaultChanges());
259+
260+
assertTrue(signer.lastInstallMessage.contains("\"approov-token\""));
261+
assertUnsignedWithoutSignatureHeaders(request, signed);
262+
}
263+
264+
@Test
265+
public void signingSkipsGracefullyWhenSignatureBaseCannotBeBuilt() throws Exception {
266+
RecordingSigner signer = new RecordingSigner();
267+
SignatureParameters missingRequiredHeader = new SignatureParameters()
268+
.addComponentIdentifier("X-Missing-Required-Header");
269+
signer.setDefaultFactory(ApproovDefaultMessageSigning.generateDefaultSignatureParametersFactory(missingRequiredHeader)
270+
.setUseAccountMessageSigning()
271+
.setAddApproovTokenHeader(false)
272+
.setAddApproovTraceIDHeader(false)
273+
.setBodyDigestConfig(null, false));
274+
signer.accountSignatureBase64 = Base64.getEncoder().encodeToString(
275+
"account-signature".getBytes(StandardCharsets.UTF_8));
276+
Request request = unsignedRequestFixture();
277+
278+
Request signed = signer.processedRequest(request, defaultChanges());
279+
280+
assertNull(signer.lastAccountMessage);
281+
assertUnsignedWithoutSignatureHeaders(request, signed);
282+
}
283+
284+
@Test
285+
public void requiredBodyDigestFailureIsFailClosed() {
286+
RecordingSigner signer = new RecordingSigner();
287+
signer.setDefaultFactory(ApproovDefaultMessageSigning.generateDefaultSignatureParametersFactory()
288+
.setBodyDigestConfig(ApproovDefaultMessageSigning.DIGEST_SHA256, true));
289+
290+
assertThrows(ApproovDefaultMessageSigning.RequiredBodyDigestException.class,
291+
() -> signer.processedRequest(requestWithoutBodyFixture(), defaultChanges()));
292+
assertNull(signer.lastInstallMessage);
293+
}
294+
295+
@Test
296+
public void unsupportedSigningAlgorithmIsFailClosed() {
297+
RecordingSigner signer = new RecordingSigner();
298+
signer.setDefaultFactory(new UnsupportedAlgorithmFactory());
299+
300+
IllegalStateException error = assertThrows(IllegalStateException.class,
301+
() -> signer.processedRequest(unsignedRequestFixture(), defaultChanges()));
302+
303+
assertTrue(error.getMessage().contains("Unsupported algorithm identifier"));
304+
assertNull(signer.lastInstallMessage);
305+
assertNull(signer.lastAccountMessage);
306+
}
170307
}

0 commit comments

Comments
 (0)