Merge ~cjwatson/launchpad:redact-signing-oops into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: b40abdf2c3300d39a703837eb686a2234d9aa089
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:redact-signing-oops
Merge into: launchpad:master
Diff against target: 222 lines (+92/-10)
2 files modified
lib/lp/services/signing/proxy.py (+7/-2)
lib/lp/services/signing/tests/test_proxy.py (+85/-8)
Reviewer Review Type Date Requested Status
Thiago F. Pappacena (community) Approve
Review via email: mp+386504@code.launchpad.net

Commit message

Redact encrypted data from signing proxy OOPSes

Description of the change

For encrypted requests to the signing service, it doesn't make sense to include the body data in timeline actions: we can't do anything with it since it's encrypted, and it may be very large.

To post a comment you must log in.
Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

LGTM

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/services/signing/proxy.py b/lib/lp/services/signing/proxy.py
2index 9c8ad6d..3147b5a 100644
3--- a/lib/lp/services/signing/proxy.py
4+++ b/lib/lp/services/signing/proxy.py
5@@ -76,12 +76,17 @@ class SigningServiceClient:
6 include a X-Response-Nonce, and returns back an encrypted
7 response JSON.
8 """
9+ headers = kwargs.get("headers", {})
10+
11 timeline = get_request_timeline(get_current_browser_request())
12+ redacted_kwargs = dict(kwargs)
13+ if "X-Client-Public-Key" in headers and "data" in redacted_kwargs:
14+ # The data will be encrypted, and possibly also very large.
15+ del redacted_kwargs["data"]
16 action = timeline.start(
17 "services-signing-proxy-%s" % method, "%s %s" %
18- (path, json.dumps(kwargs)))
19+ (path, json.dumps(redacted_kwargs)))
20
21- headers = kwargs.get("headers", {})
22 response_nonce = None
23 if "X-Response-Nonce" in headers:
24 response_nonce = base64.b64decode(headers["X-Response-Nonce"])
25diff --git a/lib/lp/services/signing/tests/test_proxy.py b/lib/lp/services/signing/tests/test_proxy.py
26index 6abc64d..9163c05 100644
27--- a/lib/lp/services/signing/tests/test_proxy.py
28+++ b/lib/lp/services/signing/tests/test_proxy.py
29@@ -18,8 +18,11 @@ from nacl.public import (
30 from nacl.utils import random
31 import responses
32 from testtools.matchers import (
33+ AfterPreprocessing,
34 ContainsDict,
35 Equals,
36+ MatchesListwise,
37+ MatchesStructure,
38 )
39 from zope.component import getUtility
40 from zope.security.proxy import removeSecurityProxy
41@@ -34,6 +37,7 @@ from lp.services.signing.interfaces.signingserviceclient import (
42 )
43 from lp.services.signing.proxy import SigningServiceClient
44 from lp.testing import TestCaseWithFactory
45+from lp.testing.fixture import CaptureTimeline
46 from lp.testing.layers import ZopelessLayer
47
48
49@@ -57,6 +61,10 @@ class SigningServiceResponseFactory:
50 self.nonce = random(Box.NONCE_SIZE)
51 self.b64_nonce = base64.b64encode(self.nonce).decode("UTF-8")
52
53+ self.response_nonce = random(Box.NONCE_SIZE)
54+ self.b64_response_nonce = base64.b64encode(
55+ self.response_nonce).decode("UTF-8")
56+
57 self.generated_public_key = PrivateKey.generate().public_key
58 self.b64_generated_public_key = base64.b64encode(
59 bytes(self.generated_public_key))
60@@ -112,11 +120,10 @@ class SigningServiceResponseFactory:
61 """
62 # Patch SigningServiceClient._makeResponseNonce to return always the
63 # same nonce, to simplify the tests.
64- response_nonce = random(Box.NONCE_SIZE)
65 test_case.useFixture(MockPatch(
66 'lp.services.signing.proxy.SigningServiceClient.'
67 '_makeResponseNonce',
68- return_value=response_nonce))
69+ return_value=self.response_nonce))
70
71 responses.add(
72 responses.GET, self.getUrl("/service-key"),
73@@ -132,14 +139,14 @@ class SigningServiceResponseFactory:
74 body=self._encryptPayload({
75 'fingerprint': self.generated_fingerprint,
76 'public-key': self.b64_generated_public_key.decode('utf8')
77- }, nonce=response_nonce),
78+ }, nonce=self.response_nonce),
79 status=201)
80
81 responses.add(
82 responses.POST, self.getUrl("/inject"),
83 body=self._encryptPayload({
84 'fingerprint': self.generated_fingerprint,
85- }, nonce=response_nonce),
86+ }, nonce=self.response_nonce),
87 status=200)
88
89 call_counts = {'/sign': 0}
90@@ -150,7 +157,7 @@ class SigningServiceResponseFactory:
91 self.signed_msg_template % call_counts['/sign'])
92 data = {'signed-message': signed.decode('utf8'),
93 'public-key': self.b64_generated_public_key.decode('utf8')}
94- return 201, {}, self._encryptPayload(data, response_nonce)
95+ return 201, {}, self._encryptPayload(data, self.response_nonce)
96
97 responses.add_callback(
98 responses.POST, self.getUrl("/sign"),
99@@ -174,6 +181,21 @@ class SigningServiceProxyTest(TestCaseWithFactory, TestWithFixtures):
100 client = removeSecurityProxy(getUtility(ISigningServiceClient))
101 self.addCleanup(client._cleanCaches)
102
103+ self.timeline = self.useFixture(CaptureTimeline()).timeline
104+
105+ def assertTimeline(self, expected_details):
106+ matchers = []
107+ for method, path, kwargs in expected_details:
108+ matchers.append(MatchesStructure(
109+ category=Equals("services-signing-proxy-%s" % method),
110+ detail=AfterPreprocessing(
111+ lambda detail: detail.split(" ", 1),
112+ MatchesListwise([
113+ Equals(path),
114+ AfterPreprocessing(json.loads, Equals(kwargs)),
115+ ]))))
116+ self.assertThat(self.timeline.actions, MatchesListwise(matchers))
117+
118 @responses.activate
119 def test_get_service_public_key(self):
120 self.response_factory.addResponses(self)
121@@ -194,6 +216,8 @@ class SigningServiceProxyTest(TestCaseWithFactory, TestWithFixtures):
122 self.assertEqual(
123 self.response_factory.getUrl("/service-key"), call.request.url)
124
125+ self.assertTimeline([("GET", "/service-key", {})])
126+
127 @responses.activate
128 def test_get_nonce(self):
129 self.response_factory.addResponses(self)
130@@ -211,6 +235,8 @@ class SigningServiceProxyTest(TestCaseWithFactory, TestWithFixtures):
131 self.assertEqual(
132 self.response_factory.getUrl("/nonce"), call.request.url)
133
134+ self.assertTimeline([("POST", "/nonce", {})])
135+
136 @responses.activate
137 def test_generate_unknown_key_type_raises_exception(self):
138 self.response_factory.addResponses(self)
139@@ -255,9 +281,26 @@ class SigningServiceProxyTest(TestCaseWithFactory, TestWithFixtures):
140 self.assertThat(http_generate.request.headers, ContainsDict({
141 "Content-Type": Equals("application/x-boxed-json"),
142 "X-Client-Public-Key": Equals(config.signing.client_public_key),
143- "X-Nonce": Equals(self.response_factory.b64_nonce)}))
144+ "X-Nonce": Equals(self.response_factory.b64_nonce),
145+ "X-Response-Nonce": Equals(
146+ self.response_factory.b64_response_nonce),
147+ }))
148 self.assertIsNotNone(http_generate.request.body)
149
150+ self.assertTimeline([
151+ ("POST", "/nonce", {}),
152+ ("GET", "/service-key", {}),
153+ ("POST", "/generate", {
154+ "headers": {
155+ "Content-Type": "application/x-boxed-json",
156+ "X-Client-Public-Key": config.signing.client_public_key,
157+ "X-Nonce": self.response_factory.b64_nonce,
158+ "X-Response-Nonce":
159+ self.response_factory.b64_response_nonce,
160+ },
161+ }),
162+ ])
163+
164 @responses.activate
165 def test_sign_invalid_mode(self):
166 signing = getUtility(ISigningServiceClient)
167@@ -313,9 +356,26 @@ class SigningServiceProxyTest(TestCaseWithFactory, TestWithFixtures):
168 self.assertThat(http_sign.request.headers, ContainsDict({
169 "Content-Type": Equals("application/x-boxed-json"),
170 "X-Client-Public-Key": Equals(config.signing.client_public_key),
171- "X-Nonce": Equals(self.response_factory.b64_nonce)}))
172+ "X-Nonce": Equals(self.response_factory.b64_nonce),
173+ "X-Response-Nonce": Equals(
174+ self.response_factory.b64_response_nonce),
175+ }))
176 self.assertIsNotNone(http_sign.request.body)
177
178+ self.assertTimeline([
179+ ("POST", "/nonce", {}),
180+ ("GET", "/service-key", {}),
181+ ("POST", "/sign", {
182+ "headers": {
183+ "Content-Type": "application/x-boxed-json",
184+ "X-Client-Public-Key": config.signing.client_public_key,
185+ "X-Nonce": self.response_factory.b64_nonce,
186+ "X-Response-Nonce":
187+ self.response_factory.b64_response_nonce,
188+ },
189+ }),
190+ ])
191+
192 # It should have returned the correct JSON content, with signed
193 # message from the API and the public-key.
194 self.assertEqual(2, len(data))
195@@ -365,9 +425,26 @@ class SigningServiceProxyTest(TestCaseWithFactory, TestWithFixtures):
196 self.assertThat(http_inject.request.headers, ContainsDict({
197 "Content-Type": Equals("application/x-boxed-json"),
198 "X-Client-Public-Key": Equals(config.signing.client_public_key),
199- "X-Nonce": Equals(self.response_factory.b64_nonce)}))
200+ "X-Nonce": Equals(self.response_factory.b64_nonce),
201+ "X-Response-Nonce": Equals(
202+ self.response_factory.b64_response_nonce),
203+ }))
204 self.assertIsNotNone(http_inject.request.body)
205
206+ self.assertTimeline([
207+ ("POST", "/nonce", {}),
208+ ("GET", "/service-key", {}),
209+ ("POST", "/inject", {
210+ "headers": {
211+ "Content-Type": "application/x-boxed-json",
212+ "X-Client-Public-Key": config.signing.client_public_key,
213+ "X-Nonce": self.response_factory.b64_nonce,
214+ "X-Response-Nonce":
215+ self.response_factory.b64_response_nonce,
216+ },
217+ }),
218+ ])
219+
220 @responses.activate
221 def test_inject_invalid_key_type(self):
222 signing = getUtility(ISigningServiceClient)

Subscribers

People subscribed via source and target branches

to status/vote changes: