maskit commented on code in PR #13548:
URL: https://github.com/apache/trafficserver/pull/13548#discussion_r3780084700


##########
tests/gold_tests/tls/tls_rpk_hop.test.py:
##########
@@ -0,0 +1,230 @@
+'''
+Test RFC 7250 raw public key (RPK) TLS between two ATS instances.
+'''
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+Test.Summary = '''
+Test raw public keys (RFC 7250) on ATS-to-ATS (layered cache) TLS hops.
+'''
+
+# RPK is only compiled in when the linked TLS library supports it, so skip 
rather
+# than fail where it is unavailable.
+Test.SkipUnless(Condition.HasATSFeature('TS_USE_RPK'))
+
+server = Test.MakeOriginServer("server")
+request_header = {'headers': 'GET / HTTP/1.1\r\nHost: 
www.example.com\r\n\r\n', 'timestamp': '1469733493.993', 'body': ''}
+response_header = {
+    'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n',
+    'timestamp': '1469733493.993',
+    'body': 'origin response'
+}
+server.addResponse("sessionlog.json", request_header, response_header)
+
+
+def make_parent(name, rpk_enabled, client_rpk_ca_file=None, 
client_cert_level=0):
+    """An upstream (parent) ATS: terminates TLS from the edge, forwards to the 
origin.
+
+    `client_rpk_ca_file`, if set, configures ssl_client_rpk_ca_name to pin the 
edge's raw public
+    key for mTLS; `client_cert_level` then requires/requests a client cert 
accordingly.
+    """
+    ts = Test.MakeATSProcess(name, enable_tls=True)
+    ts.addSSLfile("ssl/server.pem")
+    ts.addSSLfile("ssl/server.key")
+    if client_rpk_ca_file is not None:
+        ts.addSSLfile("ssl/{0}".format(client_rpk_ca_file))
+    ts.Disk.remap_config.AddLine('map / 
http://127.0.0.1:{0}'.format(server.Variables.Port))
+    multicert_lines = [
+        'ssl_multicert:',
+        '  - dest_ip: "*"',
+        '    ssl_cert_name: server.pem',
+        '    ssl_key_name: server.key',
+    ]
+    if rpk_enabled:
+        multicert_lines.append('    ssl_rpk_enabled: 1')
+    if client_rpk_ca_file is not None:
+        # The file name is deliberately bare here (not 
ts.Variables.SSLDir-prefixed) to exercise
+        # that ssl_client_rpk_ca_name resolves against 
proxy.config.ssl.CA.cert.path, matching
+        # the equivalent resolution ssl_ca_name already gets.
+        multicert_lines.append('    ssl_client_rpk_ca_name: 
{0}'.format(client_rpk_ca_file))
+    ts.Disk.ssl_multicert_yaml.AddLines(multicert_lines)
+    records = {
+        'proxy.config.http.cache.http': 0,
+        'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir),
+        'proxy.config.ssl.server.private_key.path': 
'{0}'.format(ts.Variables.SSLDir),
+        'proxy.config.diags.debug.enabled': 1,
+        'proxy.config.diags.debug.tags': 'ssl_verify|ssl_load',
+    }
+    if client_cert_level:
+        records['proxy.config.ssl.client.certification_level'] = 
client_cert_level
+        records['proxy.config.ssl.CA.cert.path'] = 
'{0}'.format(ts.Variables.SSLDir)
+    ts.Disk.records_config.update(records)
+    return ts
+
+
+def make_edge(name, parent, pin_file, policy='ENFORCED', 
offer_client_rpk=False):
+    """A downstream (edge) ATS: connects to `parent` over TLS, pinning its raw 
public key.
+
+    `offer_client_rpk`, if set, also offers a raw public key (derived from 
ssl/server.pem/.key,
+    the same identity the edge uses inbound) as its own client cert toward 
`parent`, for `parent`
+    to pin via ssl_client_rpk_ca_name.
+    """
+    ts = Test.MakeATSProcess(name, enable_tls=True)
+    ts.addSSLfile("ssl/server.pem")
+    ts.addSSLfile("ssl/server.key")
+    ts.addSSLfile("ssl/server.pubkey.pem")
+    ts.addSSLfile("ssl/server.wrongpubkey.pem")
+    ts.Disk.remap_config.AddLine('map / 
https://127.0.0.1:{0}'.format(parent.Variables.ssl_port))
+    ts.Disk.ssl_multicert_yaml.AddLines(
+        [
+            'ssl_multicert:',
+            '  - dest_ip: "*"',
+            '    ssl_cert_name: server.pem',
+            '    ssl_key_name: server.key',
+        ])
+    ts.Disk.records_config.update(
+        {
+            'proxy.config.http.cache.http': 0,
+            'proxy.config.ssl.server.cert.path': 
'{0}'.format(ts.Variables.SSLDir),
+            'proxy.config.ssl.server.private_key.path': 
'{0}'.format(ts.Variables.SSLDir),
+            'proxy.config.ssl.client.cert.path': 
'{0}'.format(ts.Variables.SSLDir),
+            'proxy.config.ssl.client.private_key.path': 
'{0}'.format(ts.Variables.SSLDir),
+            'proxy.config.diags.debug.enabled': 1,
+            'proxy.config.diags.debug.tags': 'ssl_verify',
+            'proxy.config.ssl.client.verify.server.policy': policy,
+            # Pin the exact key instead of matching a name: a raw public key 
carries no SAN.
+            'proxy.config.ssl.client.verify.server.properties': 'SIGNATURE',
+        })
+    if pin_file is not None or offer_client_rpk:
+        sni_lines = [
+            'sni:',
+            '- fqdn: 127.0.0.1',
+        ]
+        if pin_file is not None:
+            sni_lines.append('  server_rpk_ca: 
{0}/{1}'.format(ts.Variables.SSLDir, pin_file))
+        if offer_client_rpk:
+            sni_lines += [
+                '  client_cert: server.pem',
+                '  client_key: server.key',
+                '  client_rpk_enabled: true',
+            ]
+        ts.Disk.sni_yaml.AddLines(sni_lines)
+    return ts
+
+
+# 1. Both hops speak RPK and the pin matches -> RPK is negotiated and accepted.
+parent_rpk = make_parent("parent_rpk", rpk_enabled=True)
+edge_ok = make_edge("edge_ok", parent_rpk, "server.pubkey.pem")
+
+# 2. The parent has not been upgraded (no RPK), the edge is configured for it 
->
+#    negotiation must fall back to X.509 rather than failing. This is the 
steady state
+#    for the whole duration of a rolling upgrade.
+parent_x509 = make_parent("parent_x509", rpk_enabled=False)
+edge_fallback = make_edge("edge_fallback", parent_x509, "server.pubkey.pem", 
policy='PERMISSIVE')
+
+# 3. The pin does not match the key the parent presents -> rejected under 
ENFORCED.
+edge_badpin = make_edge("edge_badpin", parent_rpk, "server.wrongpubkey.pem")
+
+# 4. Same mismatch under PERMISSIVE -> warned about, but the request still 
succeeds.
+edge_badpin_permissive = make_edge("edge_badpin_permissive", parent_rpk, 
"server.wrongpubkey.pem", policy='PERMISSIVE')
+
+# 5. mTLS: the parent requires and pins the edge's raw public key, and the pin 
matches.
+parent_mtls = make_parent("parent_mtls", rpk_enabled=True, 
client_rpk_ca_file="server.pubkey.pem", client_cert_level=2)
+edge_mtls = make_edge("edge_mtls", parent_mtls, "server.pubkey.pem", 
offer_client_rpk=True)
+
+# 6. mTLS: same setup, but the parent pins a different key than the edge 
actually offers ->
+#    a required client cert is always fatal, unlike verify_server_policy which 
has a
+#    PERMISSIVE mode -- there is no equivalent "warn only" mode for inbound 
mTLS.
+parent_mtls_badpin = make_parent(
+    "parent_mtls_badpin", rpk_enabled=True, 
client_rpk_ca_file="server.wrongpubkey.pem", client_cert_level=2)
+edge_mtls_badpin = make_edge("edge_mtls_badpin", parent_mtls_badpin, 
"server.pubkey.pem", offer_client_rpk=True)
+
+tr = Test.AddTestRun("RPK negotiated and pin matches")
+tr.MakeCurlCommand('-k 
https://127.0.0.1:{0}/'.format(edge_ok.Variables.ssl_port))
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.StartBefore(server)
+tr.Processes.Default.StartBefore(parent_rpk)
+tr.Processes.Default.StartBefore(edge_ok)
+tr.Processes.Default.Streams.All = Testers.ContainsExpression('origin 
response', 'the request should succeed end to end')
+edge_ok.Disk.traffic_out.Content = Testers.ContainsExpression(
+    'Origin authenticated with a raw public key .*pin match=yes', 'the hop 
should use RPK, not fall back to X.509')
+tr.StillRunningAfter = server
+tr.StillRunningAfter += parent_rpk
+tr.StillRunningAfter += edge_ok
+
+tr = Test.AddTestRun("falls back to X.509 against a parent without RPK 
support")
+tr.MakeCurlCommand('-k 
https://127.0.0.1:{0}/'.format(edge_fallback.Variables.ssl_port))
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.StartBefore(parent_x509)
+tr.Processes.Default.StartBefore(edge_fallback)
+tr.Processes.Default.Streams.All = Testers.ContainsExpression('origin 
response', 'the request should still succeed')
+# No RPK was negotiated, so the RPK branch must never run for this hop.
+edge_fallback.Disk.traffic_out.Content = Testers.ExcludesExpression(
+    'Origin authenticated with a raw public key', 'the hop should quietly 
negotiate X.509 instead')
+tr.StillRunningAfter = server
+tr.StillRunningAfter += parent_x509
+tr.StillRunningAfter += edge_fallback
+
+tr = Test.AddTestRun("pin mismatch is fatal under ENFORCED")
+tr.MakeCurlCommand('-k 
https://127.0.0.1:{0}/'.format(edge_badpin.Variables.ssl_port))
+# curl sees a 5xx from the edge (upstream connect failed) rather than a 
transport error.
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.StartBefore(edge_badpin)

Review Comment:
   False positive: parent_rpk should not be torn down between runs



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to