<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://applied-paranoia.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://applied-paranoia.com/" rel="alternate" type="text/html" /><updated>2026-09-01T18:55:13+00:00</updated><id>https://applied-paranoia.com/feed.xml</id><title type="html">Applied paranoia</title><subtitle>Cybersecurity blog by Ronald Landheer-Cieslak.</subtitle><entry><title type="html">Radical Transparency: SCA, SBOMs and Supply Chain Security</title><link href="https://applied-paranoia.com/2026/08/23/radical-transparency.html" rel="alternate" type="text/html" title="Radical Transparency: SCA, SBOMs and Supply Chain Security" /><published>2026-08-23T00:00:00+00:00</published><updated>2026-08-23T00:00:00+00:00</updated><id>https://applied-paranoia.com/2026/08/23/radical-transparency</id><content type="html" xml:base="https://applied-paranoia.com/2026/08/23/radical-transparency.html"><![CDATA[<p>My wife is allergic to mustard. It’s a fairly common allergy, but it’s also a fairly common additive, so whenever we do the groceries, we check the ingredients of everything we buy. Having mustard as an ingredient means we can’t buy it, but so does “may contain mustard” or having “spices” as an ingredient: “may contain mustard” reads as “may require a trip to the nearest hospital,” and “spices” reads as “Russian roulette, but with food!”</p>

<p>Putting the list of ingredients on the package has been mandatory since the early twentieth century, but food “purity” laws banning certain additives from certain foods go back to the sixteenth century with the Reinheitsgebot (1516), which still governs what you can call beer in Germany. Without laws and regulations like that, we’d have a hard time finding safe food to eat.</p>

<p>Compared to food, software is obviously centuries behind: we’ve had prepared foods since the very start of humanity, while software appeared less than a century ago (two if you want to be generous with Ada Lovelace). It has, however, become a vital part of our daily lives: we can hardly do anything (including buying food) without software anymore.</p>

<p>With the ubiquity and importance of software in our daily lives and in every business, the lack of transparency about its contents is becoming a real liability. European regulations (i.e. the <a href="https://eur-lex.europa.eu/eli/reg/2024/2847/oj/eng">EU Cyber Resilience Act (CRA)</a> and the <a href="https://eur-lex.europa.eu/eli/dir/2022/2555/oj/eng">NIS2 Directive</a>) and US guidance on SBOMs (e.g. <a href="https://media.defense.gov/2026/Jul/29/2003971159/-1/-1/1/CSI_2026_cisa_sbom_minimum_elements_508c.PDF">CISA’s guidance on the minimum contents of SBOMs</a>) comprise global governments’ reactions to this liability, shifting it back from the customers and end-users of software to its creators.</p>

<p>Their approaches are different: where the EU mandates supply chain security (which includes SBOMs) and legally places the liability for vulnerabilities on the vendor, the US makes it part of their purchasing agreements, wielding the government’s purchasing power and prompting large companies (such as public utilities) to do the same. The effect is the same, though: if a software or “intelligent” equipment manufacturer wants to do business in the largest economies on the planet, they have to secure their supply chains and be transparent about the contents, the ingredients, of their software.</p>

<h2 id="the-trigger">The trigger</h2>

<p>One of the drivers for this was a wake-up call the entire software industry received with the <a href="https://attack.mitre.org/campaigns/C0024/">SUNBURST, 2019 SolarWinds compromise</a>, attributed to <a href="https://attack.mitre.org/groups/G0016/">APT29, a.k.a. Cozy Bear</a>. “APT29 used customized malware to inject malicious code into the SolarWinds Orion software build process that was later distributed through a normal software update (…). Victims of this campaign included government, consulting, technology, telecom, and other organizations in North America, Europe, Asia, and the Middle East.” (<a href="https://attack.mitre.org/campaigns/C0024/">ibid.</a>). The entire list of compromises, tools, and techniques they used makes for fascinating reading, but the end result, and the wide impact the compromise had, was the shocking part at the time: they were able to build a backdoor into the Orion software suite, which itself is a critical part of network security for many large businesses, and use it to compromise those targets.</p>

<p>SUNBURST wasn’t the only impetus for the singular focus that regulators and cybersecurity and DevSecOps professionals have shown on software supply chain security, however: a 2021 vulnerability found in Log4j, a popular logging library for Java, caused several sites to be “preventatively” shut down while fixes were deployed. I <a href="https://applied-paranoia.com/2021/12/23/log4j.html">wrote about it at the time</a> calling out “security over convenience”, and a few days later <a href="https://applied-paranoia.com/2021/12/29/getting-rid-of-java.html">explained how to get rid of Java without getting rid of Java code</a> as a mitigation to Java’s rather lax approach to security.</p>

<p>While my initial reaction at the time was to contain the threat through virtualisation (VMs, containers, …) and sandboxing, parts of the industry jumped on the software supply chain security aspect of the problem: the Log4j crisis demonstrated that vulnerabilities are often buried in subcomponents that organisations didn’t know they had.</p>

<p>Shifting responsibility for securing software from users, adopters, and customers of software vendors back to the vendors places a significant burden on the vendors’ DevSecOps practices. Individual developers will need to be sure that the changes they make to existing code don’t introduce new vulnerabilities early in the development process. Even (or especially) if they rely on AI to write part of their code, AI is trained on buggy and vulnerable code and will spit that back out when you use it. <a href="https://rlc.vlinder.ca/blog/2026/08/03/the-alien-mind">I wrote about this a few weeks ago on my other blog</a>.</p>

<p>Does that mean you should start manually checking all your dependencies, though?</p>

<h2 id="automation-reduce-time-to-market-and-reduce-developer-toil">Automation: reduce time-to-market and reduce developer toil</h2>

<p>Continuous Integration, Continuous Deployment, Continuous Testing, and Continuous Delivery all mean the same thing, but from slightly different angles: they mean automation. Automate the build, unit tests, integration tests, and system integration tests. White-box testing your code against its internal interface and class contract (unit testing), grey-box testing your components against their interface contracts (integration testing), and black-box testing the system as a whole against its highest-level contracts (system integration end-to-end testing) can and should all be automated. Only acceptance tests should involve humans once written, and even then, for the most part, they should be an inspection of the scenarios tested during end-to-end testing, followed by running those scenarios.</p>

<p>Static Analysis, Static Application Security Testing, Dynamic Application Security Testing, and Software Composition Analysis all belong in this pipeline as well. So should generating an SBOM and flagging vulnerable dependencies and their available updates.</p>

<p>The pipeline should tell you whether your code conforms to your own guidelines and consistently uses the style you approved. This is what <strong>basic static analysis</strong> gives you. C# has Roslyn Analyzers built into the .NET build system; the Java ecosystem has linters like Checkstyle, PMD, and SonarLint — pick one and stick with it; Python and Node/JavaScript/TypeScript (please use TypeScript) each come with linters in their respective ecosystems; C++ has clang-tidy as its obvious choice. Regardless of which ecosystem you use and what rules you want to impose on your code for maintainability, pick a popular, well-maintained tool and use it.</p>

<p>The pipeline should further tell you what the coverage of your unit tests is. Most unit testing frameworks can generate coverage information, and validating that coverage against a baseline minimum (which should be at least 85% for hand-written code and 95% for AI-generated code) is a simple matter of using <a href="https://github.com/vln-devsecops/actions-validate-coverage">vln-devsecops/actions-validate-coverage: Validates test coverage from Clover, Cobertura, or JaCoCo XML reports against a configurable minimum threshold, failing the workflow when coverage is below the required percentage</a>.</p>

<p>The pipeline should also indicate whether your code has recognisable defects, especially those that affect your application’s security. <strong>This is what static application security testing (SAST) brings to the table</strong>. There are plenty of free tools that help you do this. One popular option is SonarQube, a freemium tool that supports multiple languages in its free version. Typically, setting up a SAST tool like SonarQube to run on every pull request requires a bit of setup: it needs a <a href="https://www.postgresql.org/">PostgreSQL database</a> to store its configuration and previous findings, and the necessary scaffolding to run it. Nobody has time for that hassle, especially when you’re working on a zero-budget volunteer project, so I decided to create a GitHub Action for you: <a href="https://github.com/vln-devsecops/actions-sast-sonarqube">vln-devsecops/actions-sast-sonarqube: Perform SAST using SonarQube CE</a>. The action will perform a baseline scan whenever you push to your default branch (which it helpfully provides a reusable workflow to do), and use that baseline to tell you what was introduced in your PR (which it also helpfully provides a reusable workflow to do). Once written, which took my AI sidekick and me a few days, it took all of thirty minutes to roll out across all my ~150 repos.</p>

<p>The next part of the pipeline is <strong>dynamic application security testing (DAST)</strong>. The best free tool on the market seems to be the <a href="https://www.zaproxy.org/">Zed Attack Proxy (ZAP)</a>, and I intend to explore it in the future, but as with anything dynamic, it needs more scaffolding than the static tools (which typically only need access to the code and build artifacts). <em>More to come on this.</em></p>

<p>Next in line is <strong>software composition analysis (SCA)</strong> and <strong>SBOM generation</strong>. For languages that have their own package managers, like Java’s Maven or C#’s NuGet, generating an SBOM has become part and parcel of those managers: you can use <a href="https://plugins.gradle.org/plugin/org.cyclonedx.bom">the CycloneDX Gradle plugin</a> to generate an (at least initial) SBOM straight from your build process in Java, use <a href="https://github.com/CycloneDX/cyclonedx-dotnet">cyclonedx-dotnet</a> to generate one for .NET, etc. Where things get hairier is when you’re using languages not directly supported by the <a href="https://github.com/CycloneDX">CycloneDX community</a>, but even there you can use a tool like <a href="https://github.com/cdxgen/cdxgen">cdxgen</a>.</p>

<p>Going a bit further, I’ve made SCA and SBOM generation as easy as I could with <a href="https://github.com/vln-devsecops/actions-sca-syft-grype">vln-devsecops/actions-sca-syft-grype: Software composition analysis (SCA) using Syft and Grype</a>: just use the reusable workflow from the repo and you’ll get an SBOM and a vulnerability scan in Grype.</p>

<h2 id="back-to-the-kitchen">Back to the kitchen</h2>

<p>Just like cooking a good meal, development should be a worry-free exercise of creativity with a set goal. Whether that goal is to create the next great app or to feed your family, you don’t want to spend your day with churn and toil when you can be creative instead. As a developer, you want to develop “for fun and profit”. So automate the boring bits and have some fun! Integrate SA, SAST, and SCA in your automations, do automated end-to-end testing, and let automated workflows handle the churn.</p>]]></content><author><name>rlc</name></author><summary type="html"><![CDATA[My wife is allergic to mustard. It’s a fairly common allergy, but it’s also a fairly common additive, so whenever we do the groceries, we check the ingredients of everything we buy. Having mustard as an ingredient means we can’t buy it, but so does “may contain mustard” or having “spices” as an ingredient: “may contain mustard” reads as “may require a trip to the nearest hospital,” and “spices” reads as “Russian roulette, but with food!”]]></summary></entry><entry><title type="html">Unbridled optimism and friendly handshakes over beer govern the Internet</title><link href="https://applied-paranoia.com/2026/06/06/blind-optimism.html" rel="alternate" type="text/html" title="Unbridled optimism and friendly handshakes over beer govern the Internet" /><published>2026-06-06T00:00:00+00:00</published><updated>2026-06-06T00:00:00+00:00</updated><id>https://applied-paranoia.com/2026/06/06/blind-optimism</id><content type="html" xml:base="https://applied-paranoia.com/2026/06/06/blind-optimism.html"><![CDATA[<p>A paper in the latest issue of Communications of the ACM took me down a rabbit hole, and I’d like you to come with me.</p>

<p>The paper, <a href="https://dl.acm.org/doi/pdf/10.1145/3769687">“Are We Actually There? Assessing RPKI Maturity” by Schulmann et al.</a>, is an incisive and comprehensive review of the maturity of RPKI and its application to global internet routing.</p>

<p>Like most users of the internet, I have largely ignored — and frankly been ignorant of — BGP, the Border Gateway Protocol, which is used to interconnect the thousands of networks that compose the internet. It’s a protocol I typically don’t think much about, but of course I use it on a daily basis, because I use Google, email, and so on, and so do you.</p>

<p>Anyway, BGP’s lack of robust security and its vulnerability to both innocent typos and malicious attacks does not surprise me, but it also has not concerned me as much as it probably could or should.</p>

<p>The internet backbone is not secure. Back in the late 1980s when the Border Gateway Protocol was created, everything was based on optimism, a friendly smile, and a handshake — something worked out on the back of a napkin over a few beers. And honestly, some of the nicer networking protocols came about exactly like that: discussed over dinner and beers. One of the protocols I actually do think about a lot was born that way as well.</p>

<p>Anyway, back in 2012, RPKI was standardised as a suite of RFCs. The SIDR working group published the core specifications in a batch: RFC 6480 as the architectural overview, followed by a cluster of RFCs covering repository structure (6481), the ROA profile (6482), route origination validation (6483), certificate policy (6484), algorithm requirements (6485), manifests (6486), the X.509 resource certificate profile (6487), the signed object template (6488), and several more covering key rollover, trust anchor locators, and provisioning. The RPKI-to-Router protocol — which defines how a Relying Party validator feeds validated data to BGP routers — followed about a year later as RFC 6810.</p>

<p>RFC 7115 came out in 2014 and laid out the best current practice for deploying origin validation, including the fail-open posture that most networks still use today. Then in 2017, RFC 8182 introduced RRDP, which is an HTTP-based alternative to rsync for pulling down repository data. That same year, RFCs 8205 through 8209 defined BGPsec, which goes beyond just validating where a route originated and actually cryptographically authenticates the whole AS path. RFC 8210 also came out in 2017 as an update to the RTR protocol.</p>

<p>Standard tooling was added in 2018: RFC 8416 introduced SLURM, which lets operators locally override RPKI validity decisions when they need to work around known misconfigurations or complex multi-homing configurations.</p>

<p>So RPKI has been actively developed since 2012 — over a decade now, almost a decade and a half. And yet it is not that widely adopted. As the ACM paper makes clear, even after more than a decade, it is not necessarily all that mature.</p>

<p>Here is what I mean by that, and what I think the authors of the paper mean by it. When you try to find a route to some other IP address, the best route — the shortest route — is through a gateway that has the most specific announcement for that destination. Routers announce which address prefixes they have access to, and a more specific prefix (more bits) wins out over a less specific one. Fewer hops is better, and a more specific prefix gives you a better path.</p>

<p>So your routing table tells you what your best path is, but the contents of that routing table can basically come from anywhere.</p>

<p>To secure that, the messages that fill up your routing table should be authenticated. Authentication is the cornerstone of everything in security. The question is: how do you authenticate those messages, how do you sign them, and what do you do when you have no authenticated messages to work with? The RFCs try to answer that, but as Schulmann et al. show, interpretations of what they mean differ between implementers, and the software that implements them is buggy and vulnerable.</p>

<p>The core issue is how that authentication works, and whether everyone agrees on how it works. If they do not agree, some messages may be dropped that should not be, because they are actually authentic. Some messages may be accepted that should not be, because they are actually not authentic. And some messages will simply be accepted because you have no idea whether they are authentic or not — the authentication data just isn’t there.</p>

<p>If only half of service providers actually publish data to authenticate their routing updates, and only a quarter of the networks that consume routing messages actually validate any authentication at all, and those parties do not agree on how to authenticate or how to sign their messages — that is where things get messy.</p>

<p>And that is basically what this paper shows: RPKI is not mature. People do not agree on how to authenticate their messages, and people do not agree on how to validate whether those messages are authentic. It is a bit of a mess.</p>

<p>Now, I am not typically a big fan of any American administration, but the previous one did <a href="https://bidenwhitehouse.archives.gov/wp-content/uploads/2024/09/Roadmap-to-Enhancing-Internet-Routing-Security.pdf">get something right</a>. They <a href="https://docs.fcc.gov/public/attachments/DOC-402609A1.pdf">are trying to force</a> the larger internet service providers to secure the backbone and at least reduce the possibility of BGP hijacks (and, as far as I can tell, the current administration has not rolled this back).</p>

<p>Here is what I mean by that. Back in 2021, <a href="https://en.wikipedia.org/wiki/2021_Facebook_outage">Facebook had a BGP routing SNAFU</a> that removed the routes for its authoritative DNS servers. That was a mistake — a misconfiguration. The only intentional BGP hijack I am aware of is when <a href="https://www.thousandeyes.com/blog/rostelecom-route-hijack-highlights-bgp-security">Rostelecom, in April 2020, misoriginated routes</a> that diverted traffic intended for Google, Facebook, AWS, and Cloudflare through Russia. I don’t know exactly what they did with that traffic, but, well, you know: Russian.</p>

<p>The FCC has published a <a href="https://docs.fcc.gov/public/attachments/DOC-402609A1.pdf">notice of proposed rulemaking</a> that requires major providers like AT&amp;T, Comcast, Lumen, and T-Mobile to file their BGP security plans, covering how they intend to implement RPKI and handle all of that, and to update those filings on a regular basis. Those filings will be confidential, but like anything filed with the government, they will be subject to access to information requests.</p>

<p>Smaller providers — essentially the kind that serve residential customers like me — do not have to file those reports, but they must still have their plans. Providers that can formally attest that they have registered and maintained active ROAs covering at least 90% of originated routes for IP prefixes under their control can be exempt from filing those reports.</p>

<p>That creates an incentive to avoid paperwork and simply create those ROAs, which can be automated. Automating the underlying problem rather than automating the reporting is generally the better long-term approach, and appears to be the outcome they’re aiming for.</p>

<p>The incentive is right: if you can attest that 90% of your routes have ROAs, you don’t have to file.</p>

<p>You also don’t open up a potential leak of your vulnerable paths, and you don’t create a centralised repository maintained by the government — accessible through access to information requests — that essentially tells anyone who looks exactly where the vulnerabilities in the backbone are and which addresses can be hijacked.</p>

<p>The US government wanting to maintain a list like that, especially when it is not mandated by Congress and is not shielded from access to information requests or kept as an actual secret, is a real concern. The government is not good at keeping secrets.</p>

<p>The other thing that came to mind when reading this is the question of who controls the internet. There are six Regional Internet Registries that can assign numbers to Autonomous Systems which, loosely speaking, consist of network operators who route each other’s data.</p>

<p>The shortest path isn’t necessarily the best path though: you don’t necessarily want the Elbonian government, or any Elbonian entity — including whatever large Elbonian businesses you care to name — to have any control over the part of the internet backbone your data flows through.</p>

<p>So my mind went to what an authenticated but distributed system might look like, one where participants trust each other and sign off on their own data — a web of trust, basically. The route your data takes through the internet is mostly invisible to you, and you can’t choose the ASes your data will flow through, but what if you could?</p>

<p>When I thought about that for a while, I went to PGP’s web of trust model, where participants sign each other’s public keys and use their own private keys to sign messages. So, for example, AS1, AS2, AS3, and so on would sign off for each other and say, essentially, “Yes, we trust each other — at least enough to not poison the network.” Rather than routing data through ASes blindly, you’d only route through ASes you trust to not “harvest now and decrypt later”, for example.</p>

<p>That’s one way of saying: we trust each other, we can vouch for what the other party said. And therefore you as a consumer can say, “Well, I trust my own ISP and yea set of networks — and they in turn trust others, so I’ll trust them as well” with the usual web of trust mathematics: if I trust you at 100% and you trust Joe, I’ll trust Joe at 80%, and if Joe trusts Alice, I’ll trust Alice at 80% of 80%, which is 64%, and so on. At some point, if the trust score drops below, say, 50%, you start being sceptical. If I have a route through someone I trust at 80% and another through someone I trust at 40%, I’ll take the one with 80%, even if it’s an extra hop, because at least it doesn’t go through a path I don’t actually trust. This would require a level of transparency in routing we don’t currently have, but it’s a level of transparency <a href="https://arxiv.org/pdf/1508.01651">SCION</a> provides to Swiss banks.</p>

<p>The other piece, though, is how you look up that data. HTTP and rsync, the two transport mechanisms specified in the RFCs so far, aren’t necessarily all that well suited to a distributed trust model. What you really want is a distributed ledger, which is where a blockchain comes in, because that is exactly what a blockchain is. If any one access point goes down, you still have a dozen or so other access points that can serve the same data. That data is built on consensus and can contain the trust information as well.</p>

<p>As it turns out, there is <a href="https://patents.google.com/patent/US12177109B2/en">a patent from Huawei</a> that identifies exactly this idea: using a distributed ledger — a blockchain — as a decentralised peer-to-peer network of cryptographically immutable historical records that are signed, hashed, and so on.</p>

<p>Now, I don’t have a commercial interest in any of this, so I’m not going to file a patent on the web-of-trust idea. You’re welcome. 😉</p>]]></content><author><name>rlc</name></author><summary type="html"><![CDATA[A paper in the latest issue of Communications of the ACM took me down a rabbit hole, and I’d like you to come with me.]]></summary></entry><entry><title type="html">Late publication: Performing a mutually authenticated key exchange with perfect forward secrecy using a KEM</title><link href="https://applied-paranoia.com/2026/05/26/late-publication-kem-make.html" rel="alternate" type="text/html" title="Late publication: Performing a mutually authenticated key exchange with perfect forward secrecy using a KEM" /><published>2026-05-26T00:00:00+00:00</published><updated>2026-05-26T00:00:00+00:00</updated><id>https://applied-paranoia.com/2026/05/26/late-publication-kem-make</id><content type="html" xml:base="https://applied-paranoia.com/2026/05/26/late-publication-kem-make.html"><![CDATA[<p>In June 2024, I wrote a paper entitled “Performing a mutually authenticated key exchange with perfect forward secrecy using a KEM” that I have never gotten around to publishing. I should note it has not been reviewed yet, and I’d be interested in any constructive feedback (you can get in touch on LinkedIn or Blue Sky – though I may be slow to respond, I’ve been rather busy).</p>

<p><strong>Abstract</strong>:</p>
<blockquote>
  <p>The advent of quantum computing lends urgency to the development of secure protocols that can be built entirely with post-quantum algorithms. This excludes the use of the family of algorithms Diffie-Hellman and Elliptic Curve Diffie Hellman belong to, as there are no algorithms in that family that are likely to be selected in the NIST program for post-quantum cryptography, and previous candidates have been eliminated due to security issues. This paper presents a new protocol for a secure mutually authenticated key exchange with perfect forward secrecy, based on the use of KEM algorithms.</p>
</blockquote>

<p><a href="/assets/2026/kem-make.pdf">Read it here</a>.</p>

<p><strong>Note</strong>:</p>
<blockquote>
  <p>The introduction is a little out of date: ML-KEM is now the standard that CRYSTALS-Kyber was expected to become when I wrote the paper. Other than that, I believe the paper is correct.
The reasons I didn’t publish it two years ago are twofold:</p>
  <ol>
    <li>I intended to write a section on why the schemes in the original Kyber paper don’t provide perfect forward secrecy (it’s because they’re missing the two ephemenral keys and two nonces), and</li>
    <li>The paper had completely slipped my mind due to personal circumstances.</li>
  </ol>

  <p>I should also note that, as an alternative to using an AEAD for the c_m value in the third message, an HMAC over the handshake can also be used.</p>
</blockquote>]]></content><author><name>rlc</name></author><summary type="html"><![CDATA[In June 2024, I wrote a paper entitled “Performing a mutually authenticated key exchange with perfect forward secrecy using a KEM” that I have never gotten around to publishing. I should note it has not been reviewed yet, and I’d be interested in any constructive feedback (you can get in touch on LinkedIn or Blue Sky – though I may be slow to respond, I’ve been rather busy).]]></summary></entry><entry><title type="html">DNP3 and TLS</title><link href="https://applied-paranoia.com/2026/04/26/dnp3sl-and-tls.html" rel="alternate" type="text/html" title="DNP3 and TLS" /><published>2026-04-26T00:00:00+00:00</published><updated>2026-04-26T00:00:00+00:00</updated><id>https://applied-paranoia.com/2026/04/26/dnp3sl-and-tls</id><content type="html" xml:base="https://applied-paranoia.com/2026/04/26/dnp3sl-and-tls.html"><![CDATA[<p>When I joined the DNP Technical Committee in 2013, the latest version of the DNP3 standard, IEEE 1815, was less than a year old. That version of the standard contained what was then the only viable option for securing SCADA communications over serial connections. As such, DNP3-SAv5 was ahead of its time, despite its warts.</p>

<p>It didn’t take long for the committee to recognise some of the issues DNP3-SAv5 suffered from: by the time Sergey Bratus and Adam Crain published their case study of DNP3-SAv5, <a href="https://www.cs.dartmouth.edu/~sergey/langsec/papers/crain-bratus-bolt-on-dnp3sa.pdf">“Bolt-On Security Extensions for Industrial Control System Protocols”</a>, the DNP TC had already stood up the Secure Authentication Task Force (SATF; now Cybersecurity Task Force, CSTF) to study what we then thought would be SAv6.</p>

<p>The CSTF’s mandate was to design the replacement for SAv5, invite industry, protocol, and cybersecurity experts to the table, and create a secure protocol for DNP3’s unique use case. Within the DNP-UG, the CSTF is unique in that it does not require DNP-UG membership to participate in the discussion: we are intentionally open to a wider audience and have had members from every corner of the industry, including security researchers, join to help ensure the protocol’s security. Our discussions immediately turned to moving SA to its own layer and addressing some of the design issues in SAv5, making it “as simple as possible, but no simpler”.</p>

<p>In the decade that followed, we have done just that: we have removed complexity from both the Application Layer and from the security protocol itself, removed the need — or even the possibility — of pre-shared secrets, and introduced post-quantum cryptography into what has become a versatile protocol layer, DNP3-SL. This layer establishes an Association with mutual authentication using post-quantum asymmetric cryptography and, within that Association, can establish any number of subsequent Sessions using only symmetric cryptography. This design intentionally targets brownfield use cases with resource-constrained devices while still providing post-quantum security.</p>

<p>During that same time, the CSTF has also been developing a new protocol called the Authorization Management Protocol (AMP). This protocol addresses another niche that, while not unique to brownfield SCADA networks, is most prevalent there: the management of authorisation and security policies in a hierarchical network where individual devices do not have direct access to Policy Decision Points or to Certificate Authorities.</p>

<p>As with any large-scale development process, this one has taken far longer than we would have liked. We have run over the time we had allocated to the project by several years, due in part to our rigorous review process and in part to the fact that this is largely a volunteer endeavour. At every step of the way, we have answered questions, taken comments, invited participation, and endeavoured to improve the protocol. The latest input we received, admittedly through a side channel, was again <a href="https://stepfunc.io/blog/case-against-dnp3-sav6/">from Adam Crain</a>. This time, he argued that SAv6 was no longer necessary, and that he wouldn’t implement it.</p>

<p>Not implementing a layer of the DNP3 protocol is, of course, his prerogative. Some of the arguments leading him to that decision are based on faulty or outdated data, and some on his particular use case. For some use cases, you might be better off with TLS or IPsec: greenfield implementations of DNP3 that use only IP connections and don’t care about application-to-application authentication or centrally managed RBAC and security policies are well-served with transport layer-only security. The extra complexity needed to implement the security deeper down (or further up) the stack may not be worth it if the risk of compromise doesn’t warrant it.</p>

<p>DNP3-SL addresses application-to-application security that can be authorised by a central authority and authenticated using either self-signed or PKI-based certificates. It works over serial and IP connections and could be carried over TLS or IPsec. The difference between those protocols and DNP3-SL, however, lies in the design decisions we’ve made throughout the process. For example, the cryptographic primitives used target hardware acceleration available in many embedded processors, but are used sparingly to accommodate brownfield implementations. Similarly, Associations are long-lived, reducing the need to use asymmetric cryptography.</p>

<p>Many SCADA networks still use serial communications somewhere in the network. That cannot be changed with a simple firmware update. The version of DNP3 used by a device, and whether it’s configured to use DNP3-SAv5 or DNP3-SL, can.</p>

<p>It is true that there are no current commercial implementations of DNP3-SL. It may also be true that some DNP3 implementations will never support it. It is, however, very likely to be implemented by at least some popular DNP3 stack vendors and by forward-looking members of the industry.</p>

<p>DNP3-SL is the product of more than a decade of careful, open, and collaborative work by a group of professionals who care deeply about the security of critical infrastructure. It is not a perfect protocol, and it may not be the right choice for every deployment. But for many of the brownfield SCADA networks that make up the backbone of the world’s critical infrastructure, it will be the most carefully considered option available. We invite you to review the standard, participate in the CSTF, and help us make it better.</p>]]></content><author><name>rlc</name></author><summary type="html"><![CDATA[When I joined the DNP Technical Committee in 2013, the latest version of the DNP3 standard, IEEE 1815, was less than a year old. That version of the standard contained what was then the only viable option for securing SCADA communications over serial connections. As such, DNP3-SAv5 was ahead of its time, despite its warts.]]></summary></entry><entry><title type="html">Why Post-Quantum Cryptography is important</title><link href="https://applied-paranoia.com/2026/02/22/why-post-quantum.html" rel="alternate" type="text/html" title="Why Post-Quantum Cryptography is important" /><published>2026-02-22T00:00:00+00:00</published><updated>2026-02-22T00:00:00+00:00</updated><id>https://applied-paranoia.com/2026/02/22/why-post-quantum</id><content type="html" xml:base="https://applied-paranoia.com/2026/02/22/why-post-quantum.html"><![CDATA[<p>Last Friday (Feb 13), someone asked me to explain why post-quantum cryptography is important. Here’s my answer, cleaned up a bit.</p>

<h2 id="the-short-version-asymmetric-cryptography-is-in-trouble">The short version: asymmetric cryptography is in trouble</h2>

<p>Symmetric cryptography is mostly safe. Asymmetric cryptography is not and the reason comes down to trapdoor functions.</p>

<p>A trapdoor function is one that’s easy to compute in one direction but hard to reverse. The classic human analogy is multiplication versus division: multiplying a number by ten is trivial (just append a zero), while dividing is slightly more work for your brain. Scale that up to enormous numbers and throw in operations like factorization, and you have a very solid trapdoor for classical computers.</p>

<p>For quantum computers, that trapdoor is no obstacle at all. Algorithms like Shor’s can factor large numbers exponentially faster than any classical machine, which means the mathematical foundations of RSA, ECDSA, and ECDH are fundamentally vulnerable.</p>

<h2 id="we-dont-have-quantum-computers-big-enough--yet">We don’t have quantum computers big enough — yet</h2>

<p>To be clear, no quantum computer today is large enough to break these algorithms in practice. But we expect that to change around 2040 or so. That might sound far away, but the timeline for replacing cryptographic infrastructure is long, which is exactly why we need to start now.</p>

<p>There’s also the “harvest now, decrypt later” threat: an adversary can record encrypted traffic today and decrypt it once a sufficiently powerful quantum computer exists. For long-lived secrets, that’s already a serious concern.</p>

<h2 id="the-nist-competition">The NIST competition</h2>

<p>The National Institute of Standards and Technology (NIST) ran a years-long competition inviting mathematicians and cryptographers to propose new algorithms that are hard for <em>both</em> quantum and classical computers to break. The winners were <strong>CRYSTALS-Dilithium</strong> (for digital signatures) and <strong>CRYSTALS-Kyber</strong> (for key encapsulation), both based on lattice mathematics.</p>

<p>Lattice-based cryptography relies on problems in high-dimensional geometric structures that quickly become intractable. The details are complex, but the key point is that these problems appear to be hard for quantum computers just as they are for classical ones.</p>

<h2 id="whats-new-and-whats-missing">What’s new, and what’s missing</h2>

<p>The post-quantum toolkit gives us two main building blocks:</p>

<ul>
  <li><strong>Digital signature algorithms</strong> — analogous to ECDSA and RSA signatures.</li>
  <li><strong>Key encapsulation mechanisms (KEMs)</strong> — analogous to RSA-based key exchange. In a KEM, you don’t choose the shared secret yourself; the encapsulation function generates both the key and an encapsulated version of it, which the other party decapsulates using their private key.</li>
</ul>

<p>What we <em>don’t</em> have is a post-quantum equivalent of Diffie-Hellman. There was a candidate — SIDH — but it was broken by a classical computer, so it’s off the table. This means some protocols will need to be restructured to use KEM-based approaches instead of traditional key exchange.</p>

<h2 id="the-timeline">The timeline</h2>

<p>The pressure to move is real and near-term:</p>

<ul>
  <li><strong>By 2027</strong>, certificates based on classical algorithms will begin expiring on a 47-day cycle (roughly every six weeks) rather than annually. That demands significant automation investment.</li>
  <li><strong>By 2030</strong>, all new critical infrastructure projects should be using post-quantum cryptography.</li>
</ul>

<h2 id="the-case-for-hybrid-approaches">The case for hybrid approaches</h2>

<p>Because post-quantum algorithms are new, they haven’t yet accumulated the decades of cryptanalysis that classical algorithms have. Some vulnerabilities may still be undiscovered. For that reason, many practitioners, myself included, advocate for <strong>hybrid key schemes</strong> that combine a classical algorithm and a post-quantum algorithm requiring both to agree before a secret is established or a signature accepted.</p>

<p>This approach makes sense for roughly the next five years. It means you’re not betting everything on post-quantum cryptography being completely sound while classical cryptography is still holding up.</p>

<p>The catch is that once a quantum computer powerful enough to break classical algorithms actually exists, hybrid schemes no longer help you — the classical component is compromised regardless. At that point, you need to be fully on post-quantum cryptography already. Which is yet another reason not to wait.</p>

<p>Also, if your secrets need to still be secret fifteen years from now, you should be using post-quantum crypto already.</p>]]></content><author><name>rlc</name></author><summary type="html"><![CDATA[Last Friday (Feb 13), someone asked me to explain why post-quantum cryptography is important. Here’s my answer, cleaned up a bit.]]></summary></entry><entry><title type="html">Vibe-specifying Coppice</title><link href="https://applied-paranoia.com/2026/02/21/coppice.html" rel="alternate" type="text/html" title="Vibe-specifying Coppice" /><published>2026-02-21T00:00:00+00:00</published><updated>2026-02-21T00:00:00+00:00</updated><id>https://applied-paranoia.com/2026/02/21/coppice</id><content type="html" xml:base="https://applied-paranoia.com/2026/02/21/coppice.html"><![CDATA[<p>Has it really been two and a half years since I last wrote on this blog? Wow. Sorry about that, I guess!</p>

<p>This has been quite the few years, though! We’ve made <em>a lot</em> of progress with the DNP CSTF on the new DNP3 Security Layer (no longer called DNP3-SAv6) and on the Authorization Management Protocol, introducing post-quantum (or quantum-safe) crypto into the former and getting a lot closer to an Authority spec on the latter. I’ve also been involved in a number of interesting projects, most of which I can’t talk about on this public forum, and the world has mostly recovered from COVID and is now setting itself up for a whole new, but man-made, set of calamities.</p>

<p>None of that is what I want to talk about here – this is a Cybersecurity blog, not a lament-the-state-of-the-world blog.</p>

<h2 id="coppice">Coppice</h2>

<p>This morning, I had an idea. That, in itself, happens often enough, but this time, I thought I’d write about it and put it on one of my blogs (the others are in a similar state of abandon as this one, so don’t worry if you don’t follow those already).</p>

<p>A recurring theme with micro-services is the need for security and the need for availability. Both of those problems have been solved many times and in many different ways, but it feels like everyone has to re-invent the wheel every time they run into this issue. They all invariably come to one of a small set of solutions. Among those solutions are the use of service accounts and bearer tokens.</p>

<p>So, this morning, as I got out of the shower, I picked up my phone and recorded a voice memo.</p>

<p>One of the nice things with recent phones is that they automatically create a transcript of your voice memos, so I grabbed some coffee, went to my home office in the basement, and spun up a Visual Code to run this as a “vibe coding” project.</p>

<p>Here’s the transcript, edited for where Siri didn’t understand what I was saying. Before you read it (and feel free to skip it): it’s a bit of a ramble.</p>

<blockquote>
  <p>Okay, uhm. Schema: version, colon, key ID, colon, nonce, uhm, output from an HKDF. The way to calculate that is to take a secret, which stays only on the server side, assign a random nonce and, uhm, use that as the salt. Feed in the secret, the other fields, get the output, add that to the token.</p>

  <p>So on validation, we look up the key ID, we make sure that the token isn’t expired, and we run the HKDF. Output validated against what’s in the token. If it’s the same, the token is valid.</p>

  <p>We wouldn’t heat (sic.) a token that is already expired, but adding it to the token itself allows the client side to validate whether expiry has happened yet as well, which would be a nice feature to have.</p>

  <p>Uhm, yeah, so, the token doesn’t contain anything secret because the actual secret is only stored server-side, but it is a secret in the sense that it allows the bearer access to whatever, uhm, the roles or privileges assigned to the master key are, so it should be kept securely, because it itself is a secret.</p>

  <p>Okay, so, one question, maybe, is how is this better than JWT? And how is it better than a signed JWT that simply contains the claims signed on the server side.</p>

  <p>It isn’t necessarily, it’s just different.</p>

  <p>Use cases would be a bit different as well. So, one, for example, is. It’s more compact: it’s not a whole JWT. It’s not a JSON document. The same type of information, but it should take less space in a format like this.</p>

  <p>The other is, uhm, you have complete centralized control over all the tokens generated by (the service), in the sense that you can assign the roles, rather than to the token itself, you can assign it to the master key. (…) When you rotate that key, all of the tokens associated with that key automatically expire. (…)</p>

  <p>If you, uhm, if you delete the key, all of (the tokens generated with that key) would automatically go away as well, but if you assign new permissions to the key, you automatically assign new permissions to every token from that key. And that is something you can’t do with JWT.</p>

  <p>Uhm.</p>

  <p>You’ll still want to exchange it for a JWT though. So if you use it to log in on a service account, you should get a JWT back. That is much shorter lived because these tokens should live about a year. And it’s a shared secret so they, uhm, one year expiry seems reasonable. But the JWT that comes back should be, (…), perhaps refreshable access token that gives you the exact access that the master key would give you, but only lives for, like, (…), 10 minutes or an hour or whatever is appropriate for a, uhm, session timeout, in the system.</p>

  <p>Right? It does allow you to (…). That scheme where you present a token, you get a new token back, a JWT back, that’s fairly standard. Uhm, but it also allows you to let that token live for, you know, a significant amount of time. And then, uhm, you know, so a year, for example, uhm, and then just hold it for whatever time it takes.</p>

  <p>Exchanging it for a JWT token could be an action hidden from the client side. So, for example, if I send a message to my service that could (…) bearer token that is described in the entry point of that API, very well, validate that token service-side, get a JWT token back and associate it with the internal message that also gets the correlation ID. And, you know, have that go through the system.</p>

  <p>Uhm, so, that JWT doesn’t need to be visible to the client side, but it would basically allow every, uhm, microservice in the system to validate it offline that, uhm, yes, the token has been, uhm, validated and privileges associated with that authorization token, are, you know, valid and internal to the system. So the rest of the system would only see the JWT token entry point, get the bearer token, and exchange it for JWT before passing it on into the system.</p>

  <p>So that means one less handshake for, uhm, for the client. But also potential high availability for everything else because only the entry point would need access to the service that validates the token.</p>
</blockquote>

<h3 id="vibe-speccing">Vibe speccing</h3>

<p>So, I fed this into Claude and asked it to write up a spec. I was honestly a bit surprised at how decent a job it did. I had used Claude before to generate code and have often compared it to an enthusiastic intern with good Internet access and a can-do attitude, who imbibes “the good stuff” when you let him but this time, with just the transcript and a prompt, it understood the token format, HKDF key derivation, the data mode, the token lifecycle, and the hidden-from-the-client JWT token exchange flow.</p>

<p>It did add a token ID to the spec that I hadn’t asked for, which added a requirement to have a server-side list of generated tokens that the roles were associated with. It used that token ID as the nonce, rather than an actual nonce.</p>

<p>So, I told it to fix that: remove the server-side database tracking the tokens, and replace the token ID with a nonce. It understood the instructions well, but the spec it produced still had a section describing the data in a server-side database that no longer existed.</p>

<p>While it was generating that response, though, my thoughts had drifted to auditing: if I remove the server-side database tracking tokens, I also lose the traces I have of those tokens. So I told it:</p>

<blockquote>
  <p>Audit requirements are missing from the spec. Let’s add that: there should be an audit trail for any action taken on these tokens, including time stamps and the unique ID of who took the action. The audit trail should survive the master key by at least a few years.</p>
</blockquote>

<p>This triggered its first questions: How long should it retain the audit trail after key duration? (configurable, specify a recommended minimum only). Should the audit trail be accessible through the API? (No, SIEMS only). What actor identity should be captured? (Human or principal, whichever is available.)</p>

<p>It added a few new sections to the spec. I re-read its output, told it to clean up a few things (e.g. it had added “rounds” to the parameters, I told it to remove it, it asked if I wanted to move from HKDF to PBKDF2 so it could keep the parameter, I said no).</p>

<p>Finally, it had created the spec as a Word document. This blog doesn’t do Word well, and I wanted a Mermaid graph for a sequence diagram, so I asked it to re-generate the spec in Markdown and generate a README for a repo I would put it in. It did both, and I ran out of free tokens for the morning.</p>

<h3 id="the-spec">The spec</h3>

<p>Here’s the specification it came up with. It’s actually fairly close to what I might have written…</p>

<hr />

<h1 id="coppice--technical-specification">Coppice — Technical Specification</h1>

<p><strong>Version 1.3 · Draft</strong></p>

<hr />

<h2 id="table-of-contents">Table of Contents</h2>

<ol>
  <li><a href="#1-overview">Overview</a></li>
  <li><a href="#2-token-format">Token Format</a></li>
  <li><a href="#3-key-derivation-hkdf">Key Derivation (HKDF)</a></li>
  <li><a href="#4-server-side-data-model">Server-Side Data Model</a></li>
  <li><a href="#5-token-lifecycle">Token Lifecycle</a></li>
  <li><a href="#6-audit-trail">Audit Trail</a></li>
  <li><a href="#7-jwt-exchange-flow">JWT Exchange Flow</a></li>
  <li><a href="#8-api-definitions">API Definitions</a></li>
  <li><a href="#9-error-codes">Error Codes</a></li>
  <li><a href="#10-security-considerations">Security Considerations</a></li>
</ol>

<hr />

<h2 id="1-overview">1. Overview</h2>

<p>This document specifies a compact, server-authoritative authentication token scheme backed by HKDF key derivation. Tokens are opaque to clients, contain no secrets, and support centralized revocation, permission inheritance, and short-lived JWT exchange at API gateway boundaries.</p>

<p><strong>Key design goals:</strong></p>

<ul>
  <li><strong>Compactness</strong> — smaller on-wire footprint than a signed JWT containing equivalent claims.</li>
  <li><strong>Stateless token issuance</strong> — tokens are issued without writing any per-token record to the database. The server stores only a master key record per logical credential, not one record per issued token.</li>
  <li><strong>Server-authoritative permissions</strong> — roles are stored on the master key record, not embedded in the token. Any permission change propagates instantly to every token derived from that master key.</li>
  <li><strong>Centralized revocation</strong> — revoking or deleting a master key immediately invalidates all tokens derived from it.</li>
  <li><strong>No secret exposure</strong> — the system secret never appears in the token or in any database record.</li>
  <li><strong>Client transparency</strong> — long-lived tokens are exchanged for short-lived JWTs at the API gateway, so downstream microservices never see the raw token.</li>
</ul>

<hr />

<h2 id="2-token-format">2. Token Format</h2>

<h3 id="21-structure">2.1 Structure</h3>

<p>A token is a colon-delimited string, Base64url-encoded for safe transmission. The nonce provides per-token uniqueness, eliminating the need to store any per-token state server-side.</p>

<div class="language-plaintext highlighter-coderay"><table class="CodeRay"><tr>
  <td class="line-numbers"><pre><a href="#n1" name="n1">1</a>
<a href="#n2" name="n2">2</a>
<a href="#n3" name="n3">3</a>
<a href="#n4" name="n4">4</a>
</pre></td>
  <td class="code"><pre>version:masterKeyId:nonce:expiry:hash

Where each segment is Base64url-encoded before joining.
The final token = Base64url( version + &quot;:&quot; + masterKeyId + &quot;:&quot; + nonce + &quot;:&quot; + expiry + &quot;:&quot; + hash )
</pre></td>
</tr></table>
</div>

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Example</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>version</code></td>
      <td><code>1</code></td>
      <td>Integer schema version. Incremented on breaking changes to allow multi-version validation.</td>
    </tr>
    <tr>
      <td><code>masterKeyId</code></td>
      <td><code>mk_7f2a9b</code></td>
      <td>Identifies the master key record on the server. Determines which secret and permission set to use during validation.</td>
    </tr>
    <tr>
      <td><code>nonce</code></td>
      <td><code>r4Xk9p...</code></td>
      <td>Cryptographically random bytes (16 bytes / 128 bits recommended) generated at issuance. Ensures each token produces a unique HKDF output. Never reused.</td>
    </tr>
    <tr>
      <td><code>expiry</code></td>
      <td><code>1798761600</code></td>
      <td>Unix timestamp (time_t) at which the token expires. Allows client-side pre-validation without a round-trip.</td>
    </tr>
    <tr>
      <td><code>hash</code></td>
      <td><code>Ax9k...Zp4=</code></td>
      <td>HKDF-derived pseudorandom output binding all fields to the server secret. Used to validate the token server-side.</td>
    </tr>
  </tbody>
</table>

<h3 id="22-wire-format">2.2 Wire Format</h3>

<p>The assembled token must be Base64url-encoded (RFC 4648 §5, no padding) before being transmitted in an Authorization header or equivalent:</p>

<div class="language-plaintext highlighter-coderay"><table class="CodeRay"><tr>
  <td class="line-numbers"><pre><a href="#n1" name="n1">1</a>
</pre></td>
  <td class="code"><pre>Authorization: Bearer &lt;base64url(version:masterKeyId:nonce:expiry:hash)&gt;
</pre></td>
</tr></table>
</div>

<hr />

<h2 id="3-key-derivation-hkdf">3. Key Derivation (HKDF)</h2>

<h3 id="31-algorithm">3.1 Algorithm</h3>

<p>Token hashes are produced using HKDF (RFC 5869) with HMAC-SHA-256. The nonce serves as the HKDF salt, providing per-token randomness without any server-side per-token storage. The info string binds the derivation to all other token fields, ensuring that a hash cannot be transplanted to a token with different parameters.</p>

<div class="language-plaintext highlighter-coderay"><table class="CodeRay"><tr>
  <td class="line-numbers"><pre><a href="#n1" name="n1">1</a>
<a href="#n2" name="n2">2</a>
<a href="#n3" name="n3">3</a>
<a href="#n4" name="n4">4</a>
<strong><a href="#n5" name="n5">5</a></strong>
<a href="#n6" name="n6">6</a>
</pre></td>
  <td class="code"><pre>HKDF-SHA256(
  IKM  = systemSecret,          // high-entropy server-side secret; never leaves server
  salt = nonce,                  // random bytes from token; per-token uniqueness
  info = version || masterKeyId || expiry,  // pipe-delimited ASCII
  L    = 32                      // 32-byte output
)
</pre></td>
</tr></table>
</div>

<h3 id="32-hkdf-inputs">3.2 HKDF Inputs</h3>

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Type</th>
      <th>Required</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>version</code></td>
      <td>string</td>
      <td>Yes</td>
      <td>Schema version from the token. Included in info to prevent cross-version hash reuse.</td>
    </tr>
    <tr>
      <td><code>masterKeyId</code></td>
      <td>string</td>
      <td>Yes</td>
      <td>Identifies the master key record. Binds the derivation to a specific logical credential and its secret.</td>
    </tr>
    <tr>
      <td><code>nonce</code></td>
      <td>bytes</td>
      <td>Yes</td>
      <td>16 random bytes generated at issuance, used as the HKDF salt. Ensures each issued token has a unique hash even if all other fields are identical.</td>
    </tr>
    <tr>
      <td><code>expiry</code></td>
      <td>integer</td>
      <td>Yes</td>
      <td>Unix timestamp from the token. Including expiry in info prevents constructing a valid hash for a different expiry without knowing the secret.</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="4-server-side-data-model">4. Server-Side Data Model</h2>

<h3 id="41-master-key-record">4.1 Master Key Record</h3>

<p>One master key record is created per logical credential (e.g. per API client, per service account, or per tenant integration). No per-token records are stored. All tokens derived from the same master key share its permissions and revocation status.</p>

<div class="language-plaintext highlighter-coderay"><table class="CodeRay"><tr>
  <td class="line-numbers"><pre><a href="#n1" name="n1">1</a>
<a href="#n2" name="n2">2</a>
<a href="#n3" name="n3">3</a>
<a href="#n4" name="n4">4</a>
<strong><a href="#n5" name="n5">5</a></strong>
<a href="#n6" name="n6">6</a>
<a href="#n7" name="n7">7</a>
<a href="#n8" name="n8">8</a>
</pre></td>
  <td class="code"><pre>MasterKey {
  masterKeyId  string    // primary key; random, e.g. &quot;mk_7f2a9b&quot;
  version      int       // token schema version in use for this key
  tenantId     string    // scope to tenant / org
  permissions  []string  // roles / scopes granted to all tokens from this key
  revokedAt    int64?    // null = active; set to immediately revoke all tokens
  createdAt    int64
}
</pre></td>
</tr></table>
</div>

<p>Because there are no per-token records, a single master key record controls arbitrarily many issued tokens. Revoking the master key invalidates all of them simultaneously. Individual token revocation is not supported by this scheme; if per-token revocation is required, issue one master key per token.</p>

<hr />

<h2 id="5-token-lifecycle">5. Token Lifecycle</h2>

<h3 id="51-issuance">5.1 Issuance</h3>

<p>Token issuance is stateless with respect to per-token storage. The server reads the master key record but writes nothing.</p>

<ol>
  <li>Look up the MasterKey record for the requested <code>masterKeyId</code> (must exist and not be revoked).</li>
  <li>Generate a cryptographically random nonce (16 bytes, from a CSPRNG).</li>
  <li>Set expiry to <code>now + TTL</code> (default: 1 year). TTL may be overridden in the request.</li>
  <li>Run HKDF with nonce as salt and <code>version || masterKeyId || expiry</code> as info.</li>
  <li>Assemble and Base64url-encode the token: <code>version:masterKeyId:nonce:expiry:hash</code>.</li>
  <li>Return the token to the caller. No database write occurs.</li>
</ol>

<h3 id="52-validation">5.2 Validation</h3>

<ol>
  <li>Decode the Base64url token; split on <code>:</code> to extract <code>version</code>, <code>masterKeyId</code>, <code>nonce</code>, <code>expiry</code>, <code>hash</code>.</li>
  <li>Reject immediately if <code>expiry &lt; now</code> (client-side pre-check is also possible using the expiry field).</li>
  <li>Look up MasterKey by <code>masterKeyId</code>; return 401 if not found or <code>revokedAt</code> is set.</li>
  <li>Verify the version field in the token matches the version on the master key record (prevent cross-version hash reuse).</li>
  <li>Load the system secret; re-run HKDF using nonce as salt and masterKeyId with record parameters.</li>
  <li>Compare derived hash to token hash using a constant-time comparison; return 401 on mismatch.</li>
  <li>Return the <code>permissions[]</code> from the master key record to the caller.</li>
</ol>

<h3 id="53-expiry">5.3 Expiry</h3>

<p>Expiry is encoded in the token itself, allowing client-side pre-validation without a server round-trip. It is also covered by the HKDF info string, so it cannot be altered without invalidating the hash. Tokens past their expiry are rejected at validation time; no cleanup is needed.</p>

<h3 id="54-revocation">5.4 Revocation</h3>

<p>Setting <code>revokedAt</code> on a MasterKey record immediately invalidates all tokens ever issued from that key, since every validation performs a live lookup of the master key record. Deleting the record has the same effect. Because no per-token records exist, individual token revocation is not possible — revocation is always at the master key level. To approximate per-token revocation, issue a dedicated master key for each token.</p>

<hr />

<h2 id="6-audit-trail">6. Audit Trail</h2>

<p>Every action that creates, modifies, validates, revokes, or reads a master key — or that issues, validates, or exchanges a token — must produce an immutable audit event. The audit trail is the authoritative record of what happened, when, and who caused it. It must survive the deletion of any master key by a configurable retention period, with a recommended minimum of 7 years.</p>

<h3 id="61-audit-event-schema">6.1 Audit Event Schema</h3>

<p>Each audit event is a single immutable record written atomically with the action it describes. Events must never be modified or deleted within the retention window.</p>

<div class="language-plaintext highlighter-coderay"><table class="CodeRay"><tr>
  <td class="line-numbers"><pre><a href="#n1" name="n1">1</a>
<a href="#n2" name="n2">2</a>
<a href="#n3" name="n3">3</a>
<a href="#n4" name="n4">4</a>
<strong><a href="#n5" name="n5">5</a></strong>
<a href="#n6" name="n6">6</a>
<a href="#n7" name="n7">7</a>
<a href="#n8" name="n8">8</a>
<a href="#n9" name="n9">9</a>
<strong><a href="#n10" name="n10">10</a></strong>
<a href="#n11" name="n11">11</a>
<a href="#n12" name="n12">12</a>
<a href="#n13" name="n13">13</a>
<a href="#n14" name="n14">14</a>
<strong><a href="#n15" name="n15">15</a></strong>
<a href="#n16" name="n16">16</a>
</pre></td>
  <td class="code"><pre>AuditEvent {
  eventId       string    // globally unique, e.g. UUID v4
  eventType     string    // see Section 6.2
  timestamp     int64     // Unix timestamp (millisecond precision)
  masterKeyId   string?   // the key involved; null for system-level events
  tenantId      string?   // tenant scope of the affected key
  actor {
    principalId  string   // always present: service account or API credential ID
    userId       string?  // present when a human operator is identified
    ipAddress    string?  // source IP of the request
    userAgent    string?  // client user-agent string, if available
  }
  outcome       string    // &quot;success&quot; | &quot;failure&quot;
  failureReason string?   // populated on failure; maps to error codes in Section 9
  metadata      object?   // event-type-specific fields; see Section 6.3
}
</pre></td>
</tr></table>
</div>

<p>The actor block must always include at least one identity. If a request carries both a service credential and a human operator identity (e.g. via a forwarded session header), both are recorded. If only one is available, the other field is omitted — never defaulted to a placeholder.</p>

<h3 id="62-auditable-event-types">6.2 Auditable Event Types</h3>

<table>
  <thead>
    <tr>
      <th>eventType</th>
      <th>Triggered By</th>
      <th>Notes</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>master_key.created</code></td>
      <td><code>POST /master-keys</code></td>
      <td>Includes initial permissions in metadata.</td>
    </tr>
    <tr>
      <td><code>master_key.revoked</code></td>
      <td><code>DELETE /master-keys/{masterKeyId}</code></td>
      <td>All derived tokens invalidated from this point forward.</td>
    </tr>
    <tr>
      <td><code>master_key.permissions_updated</code></td>
      <td><code>PUT /master-keys/{id}/permissions</code></td>
      <td>Logs both old and new permission sets.</td>
    </tr>
    <tr>
      <td><code>master_key.looked_up</code></td>
      <td><code>GET /master-keys/{masterKeyId}</code></td>
      <td>Read-access audit; always logged regardless of outcome.</td>
    </tr>
    <tr>
      <td><code>token.issued</code></td>
      <td><code>POST /tokens/issue</code></td>
      <td>Logs masterKeyId and expiry. Token and nonce are never logged.</td>
    </tr>
    <tr>
      <td><code>token.validated</code></td>
      <td><code>POST /tokens/validate</code></td>
      <td>Logs outcome and failure reason if applicable.</td>
    </tr>
    <tr>
      <td><code>token.exchanged</code></td>
      <td><code>POST /tokens/exchange</code></td>
      <td>Logs that a JWT was minted. JWT string is never logged.</td>
    </tr>
  </tbody>
</table>

<h3 id="63-event-type-metadata">6.3 Event-Type Metadata</h3>

<p>The <code>metadata</code> field carries structured data specific to each event type.</p>

<h4 id="master_keycreated--master_keypermissions_updated"><code>master_key.created</code> / <code>master_key.permissions_updated</code></h4>

<div class="language-plaintext highlighter-coderay"><table class="CodeRay"><tr>
  <td class="line-numbers"><pre><a href="#n1" name="n1">1</a>
<a href="#n2" name="n2">2</a>
<a href="#n3" name="n3">3</a>
<a href="#n4" name="n4">4</a>
</pre></td>
  <td class="code"><pre>metadata: {
  permissions:    string[]   // new permission set
  previousPerms:  string[]?  // prior set (permissions_updated only)
}
</pre></td>
</tr></table>
</div>

<h4 id="tokenissued"><code>token.issued</code></h4>

<div class="language-plaintext highlighter-coderay"><table class="CodeRay"><tr>
  <td class="line-numbers"><pre><a href="#n1" name="n1">1</a>
<a href="#n2" name="n2">2</a>
<a href="#n3" name="n3">3</a>
<a href="#n4" name="n4">4</a>
<strong><a href="#n5" name="n5">5</a></strong>
</pre></td>
  <td class="code"><pre>metadata: {
  expiry:  int64   // token expiry as Unix timestamp
  ttl:     int64   // requested TTL in seconds
  // token string and nonce are never logged
}
</pre></td>
</tr></table>
</div>

<h4 id="tokenvalidated--tokenexchanged"><code>token.validated</code> / <code>token.exchanged</code></h4>

<div class="language-plaintext highlighter-coderay"><table class="CodeRay"><tr>
  <td class="line-numbers"><pre><a href="#n1" name="n1">1</a>
<a href="#n2" name="n2">2</a>
<a href="#n3" name="n3">3</a>
<a href="#n4" name="n4">4</a>
</pre></td>
  <td class="code"><pre>metadata: {
  expiry:        int64    // expiry from the presented token
  failureReason: string?  // see error codes in Section 9
}
</pre></td>
</tr></table>
</div>

<h3 id="64-what-must-never-be-logged">6.4 What Must Never Be Logged</h3>

<p>The following values must never appear in any audit event, log line, or diagnostic output, regardless of log level or environment:</p>

<ul>
  <li>Token strings — the Base64url-encoded credential returned to the client.</li>
  <li>Nonce values extracted from or used to construct tokens.</li>
  <li>System secret bytes or any value derived from them.</li>
  <li>JWT strings issued by the exchange endpoint.</li>
</ul>

<p>Audit events record identifiers and outcomes only — never credentials or key material.</p>

<h3 id="65-retention-requirements">6.5 Retention Requirements</h3>

<p>Audit events must be retained for the configured minimum period, measured from the <strong>later of</strong>: (a) the event timestamp, or (b) the revocation or deletion timestamp of the associated master key. This ensures the complete history of a credential remains available for investigation even after the credential itself is gone.</p>

<table>
  <thead>
    <tr>
      <th>Parameter</th>
      <th>Requirement</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Minimum retention</td>
      <td>Configurable; recommended floor of 7 years for regulated environments. Must be set at deployment time and enforced automatically.</td>
    </tr>
    <tr>
      <td>Retention anchor</td>
      <td>The later of: event timestamp or master key deletion/revocation date. Events for a key deleted after 6 years must still be retained for the full configured period from that deletion date.</td>
    </tr>
    <tr>
      <td>Immutability</td>
      <td>Events must be written to an append-only store. No update or delete operations are permitted within the retention window, including by operators.</td>
    </tr>
    <tr>
      <td>Availability</td>
      <td>Audit logs must remain queryable (not merely archived) for the full retention period.</td>
    </tr>
    <tr>
      <td>Clock accuracy</td>
      <td>All timestamps must be sourced from a synchronized clock (NTP or equivalent). Clock skew must not exceed 1 second.</td>
    </tr>
  </tbody>
</table>

<h3 id="66-delivery-and-integration">6.6 Delivery and Integration</h3>

<p>Audit events are not exposed through the Token Service API. Consumers access them out-of-band through the deployment’s log or event infrastructure. The Token Service must support at least one of the following delivery mechanisms:</p>

<ul>
  <li>Structured JSON log output to stdout — one event per line, suitable for ingestion by a log aggregator (e.g. Fluentd, Logstash, or a cloud-native log router).</li>
  <li>Direct publish to an append-only event stream (e.g. Apache Kafka, AWS Kinesis, or Azure Event Hubs) for real-time SIEM ingestion.</li>
  <li>Write to a dedicated append-only audit table in a database instance separate from the master key store, accessible to authorised compliance tooling only.</li>
</ul>

<p>The Token Service must guarantee at-least-once delivery and must emit an operational alert if the audit sink becomes unreachable. Token operations must not silently succeed with audit events dropped. If the audit sink is unavailable, the service should either fail the operation or queue the event with bounded retries and alert.</p>

<h3 id="67-access-control-on-audit-logs">6.7 Access Control on Audit Logs</h3>

<p>Read access to the audit log is a distinct privilege, granted independently of the Token Service management credential. Write access belongs exclusively to the Token Service process. No other system or operator may append to, modify, or delete records within the retention window. Human operators granted read access must not have delete access during the retention period.</p>

<hr />

<h2 id="7-jwt-exchange-flow">7. JWT Exchange Flow</h2>

<p>Long-lived tokens are not forwarded into the internal microservice mesh. Instead, the API gateway validates the token and issues a short-lived JWT that downstream services can verify offline using the gateway’s public key.</p>

<h3 id="71-flow">7.1 Flow</h3>

<p>See <a href="jwt-exchange-flow.mermaid"><code>jwt-exchange-flow.mermaid</code></a> for the sequence diagram. In summary:</p>

<ol>
  <li>Client sends a request with <code>Authorization: Bearer &lt;token&gt;</code>.</li>
  <li>API Gateway calls <code>POST /tokens/validate</code>.</li>
  <li>Token Service checks token expiry (no DB needed).</li>
  <li>Token Service looks up MasterKey, checks <code>revokedAt</code>.</li>
  <li>Token Service re-derives the HKDF hash and compares (constant-time).</li>
  <li>Token Service returns <code>{ masterKeyId, tenantId, permissions[] }</code>.</li>
  <li>API Gateway mints a short-lived JWT (exp: ~1 hour, signed with gateway private key).</li>
  <li>API Gateway forwards the request with the JWT to downstream services.</li>
  <li>Downstream services verify the JWT signature <strong>offline</strong> — no Token Service call.</li>
</ol>

<p>The JWT is invisible to the external client. It exists only within the service mesh for the duration of the request.</p>

<h3 id="72-jwt-claims">7.2 JWT Claims</h3>

<div class="language-json highlighter-coderay"><table class="CodeRay"><tr>
  <td class="line-numbers"><pre><a href="#n1" name="n1">1</a>
<a href="#n2" name="n2">2</a>
<a href="#n3" name="n3">3</a>
<a href="#n4" name="n4">4</a>
<strong><a href="#n5" name="n5">5</a></strong>
<a href="#n6" name="n6">6</a>
<a href="#n7" name="n7">7</a>
<a href="#n8" name="n8">8</a>
</pre></td>
  <td class="code"><pre>{
  <span style="color:#606"><span style="color:#404">&quot;</span><span>sub</span><span style="color:#404">&quot;</span></span>:   <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">&lt;masterKeyId&gt;</span><span style="color:#710">&quot;</span></span>,
  <span style="color:#606"><span style="color:#404">&quot;</span><span>tid</span><span style="color:#404">&quot;</span></span>:   <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">&lt;tenantId&gt;</span><span style="color:#710">&quot;</span></span>,
  <span style="color:#606"><span style="color:#404">&quot;</span><span>scope</span><span style="color:#404">&quot;</span></span>: [<span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">read:data</span><span style="color:#710">&quot;</span></span>, <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">write:data</span><span style="color:#710">&quot;</span></span>],
  <span style="color:#606"><span style="color:#404">&quot;</span><span>iat</span><span style="color:#404">&quot;</span></span>:   <span style="color:#00D">1700000000</span>,
  <span style="color:#606"><span style="color:#404">&quot;</span><span>exp</span><span style="color:#404">&quot;</span></span>:   <span style="color:#00D">1700003600</span>,
  <span style="color:#606"><span style="color:#404">&quot;</span><span>jti</span><span style="color:#404">&quot;</span></span>:   <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">&lt;random nonce&gt;</span><span style="color:#710">&quot;</span></span>
}
</pre></td>
</tr></table>
</div>

<hr />

<h2 id="8-api-definitions">8. API Definitions</h2>

<p>The Token Service exposes the following REST endpoints. All requests and responses use <code>application/json</code>. All management endpoints require a service-to-service credential (e.g. mTLS or an internal bearer token).</p>

<h3 id="81-endpoint-summary">8.1 Endpoint Summary</h3>

<table>
  <thead>
    <tr>
      <th>Method</th>
      <th>Path</th>
      <th>Auth</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>POST</code></td>
      <td><code>/master-keys</code></td>
      <td>Service credential</td>
      <td>Create a master key record</td>
    </tr>
    <tr>
      <td><code>GET</code></td>
      <td><code>/master-keys/{masterKeyId}</code></td>
      <td>Service credential</td>
      <td>Retrieve master key metadata</td>
    </tr>
    <tr>
      <td><code>PUT</code></td>
      <td><code>/master-keys/{masterKeyId}/permissions</code></td>
      <td>Service credential</td>
      <td>Update permissions on a master key</td>
    </tr>
    <tr>
      <td><code>DELETE</code></td>
      <td><code>/master-keys/{masterKeyId}</code></td>
      <td>Service credential</td>
      <td>Revoke all tokens for a master key</td>
    </tr>
    <tr>
      <td><code>POST</code></td>
      <td><code>/tokens/issue</code></td>
      <td>Service credential</td>
      <td>Issue a new token from a master key (no DB write)</td>
    </tr>
    <tr>
      <td><code>POST</code></td>
      <td><code>/tokens/validate</code></td>
      <td>None</td>
      <td>Validate a token; returns permissions</td>
    </tr>
    <tr>
      <td><code>POST</code></td>
      <td><code>/tokens/exchange</code></td>
      <td>Bearer token</td>
      <td>Exchange a long-lived token for a JWT</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="82-post-master-keys--create-master-key">8.2 <code>POST /master-keys</code> — Create Master Key</h3>

<p>Creates a new master key record. A master key is the logical credential from which any number of tokens can be issued. No tokens are produced by this call.</p>

<p><strong>Request</strong></p>

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Type</th>
      <th>Required</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>tenantId</code></td>
      <td>string</td>
      <td>Yes</td>
      <td>Tenant or organisation scope for all tokens derived from this key.</td>
    </tr>
    <tr>
      <td><code>permissions</code></td>
      <td>string[]</td>
      <td>Yes</td>
      <td>Initial list of roles/scopes granted to all tokens from this key.</td>
    </tr>
  </tbody>
</table>

<div class="language-http highlighter-coderay"><table class="CodeRay"><tr>
  <td class="line-numbers"><pre><a href="#n1" name="n1">1</a>
<a href="#n2" name="n2">2</a>
<a href="#n3" name="n3">3</a>
<a href="#n4" name="n4">4</a>
<strong><a href="#n5" name="n5">5</a></strong>
<a href="#n6" name="n6">6</a>
<a href="#n7" name="n7">7</a>
</pre></td>
  <td class="code"><pre>POST /master-keys
Content-Type: application/json

{
  &quot;tenantId&quot;: &quot;acme-corp&quot;,
  &quot;permissions&quot;: [&quot;read:reports&quot;, &quot;write:data&quot;]
}
</pre></td>
</tr></table>
</div>

<p><strong>Response <code>201 Created</code></strong></p>

<div class="language-json highlighter-coderay"><table class="CodeRay"><tr>
  <td class="line-numbers"><pre><a href="#n1" name="n1">1</a>
<a href="#n2" name="n2">2</a>
<a href="#n3" name="n3">3</a>
<a href="#n4" name="n4">4</a>
<strong><a href="#n5" name="n5">5</a></strong>
<a href="#n6" name="n6">6</a>
</pre></td>
  <td class="code"><pre>{
  <span style="color:#606"><span style="color:#404">&quot;</span><span>masterKeyId</span><span style="color:#404">&quot;</span></span>: <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">mk_7f2a9b</span><span style="color:#710">&quot;</span></span>,
  <span style="color:#606"><span style="color:#404">&quot;</span><span>tenantId</span><span style="color:#404">&quot;</span></span>:    <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">acme-corp</span><span style="color:#710">&quot;</span></span>,
  <span style="color:#606"><span style="color:#404">&quot;</span><span>permissions</span><span style="color:#404">&quot;</span></span>: [<span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">read:reports</span><span style="color:#710">&quot;</span></span>, <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">write:data</span><span style="color:#710">&quot;</span></span>],
  <span style="color:#606"><span style="color:#404">&quot;</span><span>createdAt</span><span style="color:#404">&quot;</span></span>:   <span style="color:#00D">1700000000</span>
}
</pre></td>
</tr></table>
</div>

<hr />

<h3 id="83-post-tokensissue--issue-token">8.3 <code>POST /tokens/issue</code> — Issue Token</h3>

<p>Issues a new token from an existing master key. The server generates a nonce, computes the HKDF hash, and returns the encoded token. No per-token record is written to the database.</p>

<p><strong>Request</strong></p>

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Type</th>
      <th>Required</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>masterKeyId</code></td>
      <td>string</td>
      <td>Yes</td>
      <td>The master key from which to derive this token.</td>
    </tr>
    <tr>
      <td><code>ttlSeconds</code></td>
      <td>integer</td>
      <td>No</td>
      <td>Token lifetime in seconds. Default: <code>31536000</code> (1 year).</td>
    </tr>
  </tbody>
</table>

<div class="language-http highlighter-coderay"><table class="CodeRay"><tr>
  <td class="line-numbers"><pre><a href="#n1" name="n1">1</a>
<a href="#n2" name="n2">2</a>
<a href="#n3" name="n3">3</a>
<a href="#n4" name="n4">4</a>
<strong><a href="#n5" name="n5">5</a></strong>
<a href="#n6" name="n6">6</a>
<a href="#n7" name="n7">7</a>
</pre></td>
  <td class="code"><pre>POST /tokens/issue
Content-Type: application/json

{
  &quot;masterKeyId&quot;: &quot;mk_7f2a9b&quot;,
  &quot;ttlSeconds&quot;:  31536000
}
</pre></td>
</tr></table>
</div>

<p><strong>Response <code>201 Created</code></strong></p>

<div class="language-json highlighter-coderay"><table class="CodeRay"><tr>
  <td class="line-numbers"><pre><a href="#n1" name="n1">1</a>
<a href="#n2" name="n2">2</a>
<a href="#n3" name="n3">3</a>
<a href="#n4" name="n4">4</a>
<strong><a href="#n5" name="n5">5</a></strong>
</pre></td>
  <td class="code"><pre>{
  <span style="color:#606"><span style="color:#404">&quot;</span><span>token</span><span style="color:#404">&quot;</span></span>:       <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">djErbWtfN2YyYTliOn...</span><span style="color:#710">&quot;</span></span>,
  <span style="color:#606"><span style="color:#404">&quot;</span><span>masterKeyId</span><span style="color:#404">&quot;</span></span>: <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">mk_7f2a9b</span><span style="color:#710">&quot;</span></span>,
  <span style="color:#606"><span style="color:#404">&quot;</span><span>expiry</span><span style="color:#404">&quot;</span></span>:      <span style="color:#00D">1798761600</span>
}
</pre></td>
</tr></table>
</div>

<hr />

<h3 id="84-post-tokensvalidate--validate-token">8.4 <code>POST /tokens/validate</code> — Validate Token</h3>

<p>Validates a raw token and returns the associated permissions. The API gateway calls this on every inbound request before minting a JWT. Since no per-token state exists, this call performs only a master key lookup and HKDF re-derivation.</p>

<p><strong>Request</strong></p>

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Type</th>
      <th>Required</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>token</code></td>
      <td>string</td>
      <td>Yes</td>
      <td>The raw Base64url-encoded token presented by the client.</td>
    </tr>
  </tbody>
</table>

<div class="language-http highlighter-coderay"><table class="CodeRay"><tr>
  <td class="line-numbers"><pre><a href="#n1" name="n1">1</a>
<a href="#n2" name="n2">2</a>
<a href="#n3" name="n3">3</a>
<a href="#n4" name="n4">4</a>
<strong><a href="#n5" name="n5">5</a></strong>
<a href="#n6" name="n6">6</a>
</pre></td>
  <td class="code"><pre>POST /tokens/validate
Content-Type: application/json

{
  &quot;token&quot;: &quot;djErbWtfN2YyYTliOn...&quot;
}
</pre></td>
</tr></table>
</div>

<p><strong>Response <code>200 OK</code></strong></p>

<div class="language-json highlighter-coderay"><table class="CodeRay"><tr>
  <td class="line-numbers"><pre><a href="#n1" name="n1">1</a>
<a href="#n2" name="n2">2</a>
<a href="#n3" name="n3">3</a>
<a href="#n4" name="n4">4</a>
<strong><a href="#n5" name="n5">5</a></strong>
<a href="#n6" name="n6">6</a>
<a href="#n7" name="n7">7</a>
</pre></td>
  <td class="code"><pre>{
  <span style="color:#606"><span style="color:#404">&quot;</span><span>valid</span><span style="color:#404">&quot;</span></span>:        <span style="color:#088">true</span>,
  <span style="color:#606"><span style="color:#404">&quot;</span><span>masterKeyId</span><span style="color:#404">&quot;</span></span>:  <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">mk_7f2a9b</span><span style="color:#710">&quot;</span></span>,
  <span style="color:#606"><span style="color:#404">&quot;</span><span>tenantId</span><span style="color:#404">&quot;</span></span>:     <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">acme-corp</span><span style="color:#710">&quot;</span></span>,
  <span style="color:#606"><span style="color:#404">&quot;</span><span>permissions</span><span style="color:#404">&quot;</span></span>:  [<span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">read:reports</span><span style="color:#710">&quot;</span></span>, <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">write:data</span><span style="color:#710">&quot;</span></span>],
  <span style="color:#606"><span style="color:#404">&quot;</span><span>expiry</span><span style="color:#404">&quot;</span></span>:       <span style="color:#00D">1798761600</span>
}
</pre></td>
</tr></table>
</div>

<p><strong>Response <code>401 Unauthorized</code></strong></p>

<div class="language-json highlighter-coderay"><table class="CodeRay"><tr>
  <td class="line-numbers"><pre><a href="#n1" name="n1">1</a>
<a href="#n2" name="n2">2</a>
<a href="#n3" name="n3">3</a>
<a href="#n4" name="n4">4</a>
</pre></td>
  <td class="code"><pre>{
  <span style="color:#606"><span style="color:#404">&quot;</span><span>valid</span><span style="color:#404">&quot;</span></span>:  <span style="color:#088">false</span>,
  <span style="color:#606"><span style="color:#404">&quot;</span><span>reason</span><span style="color:#404">&quot;</span></span>: <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">expired</span><span style="color:#710">&quot;</span></span> <span style="color:#F00;background-color:#FAA">|</span> <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">revoked</span><span style="color:#710">&quot;</span></span> <span style="color:#F00;background-color:#FAA">|</span> <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">not_found</span><span style="color:#710">&quot;</span></span> <span style="color:#F00;background-color:#FAA">|</span> <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">hash_mismatch</span><span style="color:#710">&quot;</span></span> <span style="color:#F00;background-color:#FAA">|</span> <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">version_mismatch</span><span style="color:#710">&quot;</span></span>
}
</pre></td>
</tr></table>
</div>

<hr />

<h3 id="85-post-tokensexchange--exchange-for-jwt">8.5 <code>POST /tokens/exchange</code> — Exchange for JWT</h3>

<p>Accepts a long-lived token in the Authorization header and returns a short-lived, signed JWT for use within the service mesh. This is the primary entry-point for all authenticated API calls.</p>

<p><strong>Request Headers</strong></p>

<table>
  <thead>
    <tr>
      <th>Header</th>
      <th>Required</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>Authorization</code></td>
      <td>Yes</td>
      <td><code>Bearer &lt;base64url-token&gt;</code></td>
    </tr>
  </tbody>
</table>

<div class="language-http highlighter-coderay"><table class="CodeRay"><tr>
  <td class="line-numbers"><pre><a href="#n1" name="n1">1</a>
<a href="#n2" name="n2">2</a>
</pre></td>
  <td class="code"><pre>POST /tokens/exchange
Authorization: Bearer djErbWtfN2YyYTliOn...
</pre></td>
</tr></table>
</div>

<p><strong>Response <code>200 OK</code></strong></p>

<div class="language-json highlighter-coderay"><table class="CodeRay"><tr>
  <td class="line-numbers"><pre><a href="#n1" name="n1">1</a>
<a href="#n2" name="n2">2</a>
<a href="#n3" name="n3">3</a>
<a href="#n4" name="n4">4</a>
</pre></td>
  <td class="code"><pre>{
  <span style="color:#606"><span style="color:#404">&quot;</span><span>jwt</span><span style="color:#404">&quot;</span></span>:       <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">eyJhbGciOiJSUzI1NiJ9...</span><span style="color:#710">&quot;</span></span>,
  <span style="color:#606"><span style="color:#404">&quot;</span><span>expiresIn</span><span style="color:#404">&quot;</span></span>: <span style="color:#00D">3600</span>
}
</pre></td>
</tr></table>
</div>

<hr />

<h3 id="86-get-master-keysmasterkeyid--get-master-key">8.6 <code>GET /master-keys/{masterKeyId}</code> — Get Master Key</h3>

<p>Returns metadata for a master key record. Never returns secret material.</p>

<p><strong>Response <code>200 OK</code></strong></p>

<div class="language-json highlighter-coderay"><table class="CodeRay"><tr>
  <td class="line-numbers"><pre><a href="#n1" name="n1">1</a>
<a href="#n2" name="n2">2</a>
<a href="#n3" name="n3">3</a>
<a href="#n4" name="n4">4</a>
<strong><a href="#n5" name="n5">5</a></strong>
<a href="#n6" name="n6">6</a>
<a href="#n7" name="n7">7</a>
<a href="#n8" name="n8">8</a>
</pre></td>
  <td class="code"><pre>{
  <span style="color:#606"><span style="color:#404">&quot;</span><span>masterKeyId</span><span style="color:#404">&quot;</span></span>: <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">mk_7f2a9b</span><span style="color:#710">&quot;</span></span>,
  <span style="color:#606"><span style="color:#404">&quot;</span><span>tenantId</span><span style="color:#404">&quot;</span></span>:    <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">acme-corp</span><span style="color:#710">&quot;</span></span>,
  <span style="color:#606"><span style="color:#404">&quot;</span><span>version</span><span style="color:#404">&quot;</span></span>:     <span style="color:#00D">1</span>,
  <span style="color:#606"><span style="color:#404">&quot;</span><span>permissions</span><span style="color:#404">&quot;</span></span>: [<span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">read:reports</span><span style="color:#710">&quot;</span></span>, <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">write:data</span><span style="color:#710">&quot;</span></span>],
  <span style="color:#606"><span style="color:#404">&quot;</span><span>revokedAt</span><span style="color:#404">&quot;</span></span>:   <span style="color:#088">null</span>,
  <span style="color:#606"><span style="color:#404">&quot;</span><span>createdAt</span><span style="color:#404">&quot;</span></span>:   <span style="color:#00D">1700000000</span>
}
</pre></td>
</tr></table>
</div>

<hr />

<h3 id="87-put-master-keysmasterkeyidpermissions--update-permissions">8.7 <code>PUT /master-keys/{masterKeyId}/permissions</code> — Update Permissions</h3>

<p>Replaces the permission set on a master key record. The change takes effect on the next validation for any token derived from this key — no token re-issuance required. For security, automated flows should only permit permission reduction; permission elevation should require explicit operator approval.</p>

<p><strong>Request</strong></p>

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Type</th>
      <th>Required</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>permissions</code></td>
      <td>string[]</td>
      <td>Yes</td>
      <td>Complete replacement set of permissions for all tokens derived from this master key.</td>
    </tr>
  </tbody>
</table>

<div class="language-http highlighter-coderay"><table class="CodeRay"><tr>
  <td class="line-numbers"><pre><a href="#n1" name="n1">1</a>
<a href="#n2" name="n2">2</a>
<a href="#n3" name="n3">3</a>
<a href="#n4" name="n4">4</a>
<strong><a href="#n5" name="n5">5</a></strong>
<a href="#n6" name="n6">6</a>
</pre></td>
  <td class="code"><pre>PUT /master-keys/mk_7f2a9b/permissions
Content-Type: application/json

{
  &quot;permissions&quot;: [&quot;read:reports&quot;]
}
</pre></td>
</tr></table>
</div>

<p><strong>Response <code>200 OK</code></strong></p>

<div class="language-json highlighter-coderay"><table class="CodeRay"><tr>
  <td class="line-numbers"><pre><a href="#n1" name="n1">1</a>
<a href="#n2" name="n2">2</a>
<a href="#n3" name="n3">3</a>
<a href="#n4" name="n4">4</a>
<strong><a href="#n5" name="n5">5</a></strong>
</pre></td>
  <td class="code"><pre>{
  <span style="color:#606"><span style="color:#404">&quot;</span><span>masterKeyId</span><span style="color:#404">&quot;</span></span>: <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">mk_7f2a9b</span><span style="color:#710">&quot;</span></span>,
  <span style="color:#606"><span style="color:#404">&quot;</span><span>permissions</span><span style="color:#404">&quot;</span></span>: [<span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">read:reports</span><span style="color:#710">&quot;</span></span>],
  <span style="color:#606"><span style="color:#404">&quot;</span><span>updatedAt</span><span style="color:#404">&quot;</span></span>:   <span style="color:#00D">1700005000</span>
}
</pre></td>
</tr></table>
</div>

<hr />

<h3 id="88-delete-master-keysmasterkeyid--revoke-master-key">8.8 <code>DELETE /master-keys/{masterKeyId}</code> — Revoke Master Key</h3>

<p>Immediately revokes the master key by setting <code>revokedAt</code>. All subsequent validation calls for any token derived from this <code>masterKeyId</code> will return 401. This is the only revocation granularity available; there is no per-token revocation.</p>

<p><strong>Response <code>204 No Content</code></strong></p>

<p>Empty body on success.</p>

<p><strong>Response <code>404 Not Found</code></strong></p>

<div class="language-json highlighter-coderay"><table class="CodeRay"><tr>
  <td class="line-numbers"><pre><a href="#n1" name="n1">1</a>
<a href="#n2" name="n2">2</a>
<a href="#n3" name="n3">3</a>
</pre></td>
  <td class="code"><pre>{
  <span style="color:#606"><span style="color:#404">&quot;</span><span>error</span><span style="color:#404">&quot;</span></span>: <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">master_key_not_found</span><span style="color:#710">&quot;</span></span>
}
</pre></td>
</tr></table>
</div>

<hr />

<h2 id="9-error-codes">9. Error Codes</h2>

<table>
  <thead>
    <tr>
      <th>HTTP Status</th>
      <th><code>error</code> / <code>reason</code></th>
      <th>Meaning</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>400</code></td>
      <td><code>invalid_token_format</code></td>
      <td>Token could not be decoded or parsed (wrong number of segments, invalid Base64url).</td>
    </tr>
    <tr>
      <td><code>401</code></td>
      <td><code>expired</code></td>
      <td>Token expiry timestamp is in the past.</td>
    </tr>
    <tr>
      <td><code>401</code></td>
      <td><code>revoked</code></td>
      <td>Master key record has a non-null <code>revokedAt</code>; all derived tokens are invalidated.</td>
    </tr>
    <tr>
      <td><code>401</code></td>
      <td><code>not_found</code></td>
      <td>No master key record exists for the given <code>masterKeyId</code>.</td>
    </tr>
    <tr>
      <td><code>401</code></td>
      <td><code>hash_mismatch</code></td>
      <td>HKDF re-derivation did not match the token hash.</td>
    </tr>
    <tr>
      <td><code>401</code></td>
      <td><code>version_mismatch</code></td>
      <td>Token version does not match the master key record’s version.</td>
    </tr>
    <tr>
      <td><code>404</code></td>
      <td><code>master_key_not_found</code></td>
      <td>Requested <code>masterKeyId</code> does not exist (management endpoints).</td>
    </tr>
    <tr>
      <td><code>500</code></td>
      <td><code>internal_error</code></td>
      <td>Unexpected server-side failure.</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="10-security-considerations">10. Security Considerations</h2>

<h3 id="101-token-secrecy">10.1 Token Secrecy</h3>

<p>Although tokens contain no embedded secrets, possession of a token grants access to all resources the associated master key permits. Tokens must be treated as secrets by clients: stored securely (e.g. OS keychain or encrypted storage), never logged, and transmitted only over TLS.</p>

<h3 id="102-nonce-requirements">10.2 Nonce Requirements</h3>

<p>The nonce is the sole source of per-token uniqueness. It must be generated using a cryptographically secure pseudorandom number generator (CSPRNG) and must never be reused across tokens issued from the same master key. A minimum of 16 bytes (128 bits) is required. Because the nonce is included in the token and used as the HKDF salt, it need not be stored server-side, but it must be resistant to prediction.</p>

<h3 id="103-revocation-granularity">10.3 Revocation Granularity</h3>

<p>Revocation operates at the master key level. Revoking a master key invalidates all tokens ever issued from it. Individual token revocation is not possible without maintaining per-token state, which this scheme deliberately avoids. Systems requiring per-token revocation should issue one master key per token. This is a deliberate trade-off: stateless issuance at the cost of granular revocation.</p>

<h3 id="104-vs-signed-jwt">10.4 vs. Signed JWT</h3>

<p>This scheme differs from signed JWTs in several important ways. JWTs embed claims and permissions in the token itself, meaning permissions cannot be changed without re-issuing the token. This scheme stores permissions server-side on the master key record, enabling instant permission changes across all derived tokens. The JWT-exchange flow (Section 7) preserves offline validation for internal services: only the API gateway validates the long-lived token; downstream services validate the short-lived JWT offline without a database call.</p>

<h3 id="105-minimum-service-access">10.5 Minimum Service Access</h3>

<p>Only the token validation microservice requires access to the system secret store and the master key database. All other internal services interact exclusively with the short-lived JWT. This minimises the blast radius of a compromise.</p>

<h3 id="106-secret-rotation--known-constraint">10.6 Secret Rotation — Known Constraint</h3>

<p>Because the master key record does not store a reference to the secret used at issuance, rotating the system secret is a <strong>breaking operation</strong>: all existing tokens will fail HKDF re-derivation and return <code>hash_mismatch</code> on their next validation. Secret rotation therefore requires coordinated re-issuance of all active tokens before the old secret is retired. Operators should treat a secret rotation event as equivalent to revoking all master keys and re-provisioning all clients. If non-breaking rotation is required, a <code>secretId</code> reference should be reintroduced to the MasterKey record.</p>

<h3 id="107-constant-time-comparison">10.7 Constant-Time Comparison</h3>

<p>Hash comparison during validation must use a constant-time byte comparison function to prevent timing-based oracle attacks.</p>

<h3 id="108-tenant-scoping">10.8 Tenant Scoping</h3>

<p>All master key records are scoped to a <code>tenantId</code>. Validation endpoints must enforce tenant isolation: a token issued for tenant A must never be accepted for tenant B’s resources.</p>

<hr />

<p><em>Coppice — HKDF Token Scheme · v1.3 Draft</em></p>]]></content><author><name>rlc</name></author><summary type="html"><![CDATA[Has it really been two and a half years since I last wrote on this blog? Wow. Sorry about that, I guess!]]></summary></entry><entry><title type="html">Replacing a DH with a KEM in protocols- how and why</title><link href="https://applied-paranoia.com/2023/09/16/ecdh-kem.html" rel="alternate" type="text/html" title="Replacing a DH with a KEM in protocols- how and why" /><published>2023-09-16T00:00:00+00:00</published><updated>2023-09-16T00:00:00+00:00</updated><id>https://applied-paranoia.com/2023/09/16/ecdh-kem</id><content type="html" xml:base="https://applied-paranoia.com/2023/09/16/ecdh-kem.html"><![CDATA[<p>The advent of quantum computing presents new challenges for secure OT protocols. The current lack of a secure post-quantum replacement of the Diﬀie-Hellman key exchange (and its ECDH counterpart) means that novel protocols such as DNP3-SAv6 may have to integrate alternative approaches such as Key Encapsulation Mechanisms (KEMs). This paper presents a means to simulate a KEM using only DH constructs to allow for experimentation as such alternative approaches are developed, reviews existing analyses for related protocols and analyzes how the lessons learned from those analyses apply to novel OT protocols like DNP3-SAv6 and IEC 62351-5, and presents recommendations for changes to DNP3-SAv6 ahead of that protocol’s publication in the upcoming DNP3 standard to accomodate necessary changes to be made when more thorough cryptanalysis on post-quantum cryptographic standards is available.</p>

<p><a href="https://rlc.vlinder.ca/ecdh-kem/">more…</a></p>]]></content><author><name>rlc</name></author><summary type="html"><![CDATA[The advent of quantum computing presents new challenges for secure OT protocols. The current lack of a secure post-quantum replacement of the Diﬀie-Hellman key exchange (and its ECDH counterpart) means that novel protocols such as DNP3-SAv6 may have to integrate alternative approaches such as Key Encapsulation Mechanisms (KEMs). This paper presents a means to simulate a KEM using only DH constructs to allow for experimentation as such alternative approaches are developed, reviews existing analyses for related protocols and analyzes how the lessons learned from those analyses apply to novel OT protocols like DNP3-SAv6 and IEC 62351-5, and presents recommendations for changes to DNP3-SAv6 ahead of that protocol’s publication in the upcoming DNP3 standard to accomodate necessary changes to be made when more thorough cryptanalysis on post-quantum cryptographic standards is available.]]></summary></entry><entry><title type="html">Should you encrypt everything?</title><link href="https://applied-paranoia.com/2023/05/13/should-you-encrypt-everything.html" rel="alternate" type="text/html" title="Should you encrypt everything?" /><published>2023-05-13T00:00:00+00:00</published><updated>2023-05-13T00:00:00+00:00</updated><id>https://applied-paranoia.com/2023/05/13/should-you-encrypt-everything</id><content type="html" xml:base="https://applied-paranoia.com/2023/05/13/should-you-encrypt-everything.html"><![CDATA[<p>Encryption is the mechanism by which normal data, like this text, is made to look like random gibberish – arguably also like his text. The basic idea of encryption has been around ever since the first secret was confidentially told to someone in earshot of someone else: when the first “Alice” had a message for the first “Bob” that she didn’t want the first “Eve” to understand, even if she could overhear it.</p>

<p>One of the most famous encryption mechanisms in history is the German Enygma machine, used by the Nazis during the Second World War to encrypt messages between different parts of the German army, and eventually cracked by the British under the leadership and guidance of Alan Turing, one of the fathers of modern computing. Since those days which, while less than a century ago and still part of living memory today, are ancient history for the annals of computing, encryption has made great strides with the development of various “block” and “stream” ciphers (i.e. encryption algorithms), and various “modes” which each have their advantages and drawbacks. All of these algorithms essentially do the same thing, though: using some mathematical trickery, they obfuscate the true meaning, and sometimes the true size, of a message or some data at rest, providing confidentiality to those who have the key to unlock that true meaning, against those who don’t.</p>

<p>But when is it worth doing that?</p>

<p>In information security (a.k.a. InfoSec), we learned a long time ago (again, computer years – it may only jhave been a decade or so) that the mantra “I have nothing to hide” is false on its face: you identify yourself to your bank with a four or five-digit PIN and a physical card, and expect your bank to believe you when you contest a fraudulent charge made with that same card and PIN. Thanks to Artificial Intelligence and Machine Learning, they probably will, but keeping your PIN secret is also part of that equation. Similarly, your IT department will tell you to change your password often, and keep it a secret. Passwords date back to the 1960s and are due for an overhaul, but they’re still secrets you need to keep. So, it’s probably fair to say everyone has secrets to keep. The answer to keeping secrets, or maintaining confidentiality of those secrets, when stored or when in transit (e.g. while being shared between you and the bank), has been the same since before computer science was a thing: encryption.</p>

<p>The IT, InfoSec, and more generally cybersecurity folks might just go ahead and encrypt everything, then. And why not? Today’s iPhone and Android smart phones have more computing power than a supercomputer from the 1970s by a factor of more than a thousand. The computational overhead of encrypting and decrypting everything is negligeable. But that does mean you should just encrypt everything?</p>

<p>Let’s take a look at the encryption you’re using right now: the Applied Paranoia blog is public. There are no secrets on this website: any secrets that are part of the CI/CD pipeline are not on the site itself – I’m paranoid enough to have checked. Yet, the website uses TLS for the connection between the Amazon-hosted server and your browser. TLS encrypts everything, but that’s not its purpose in this case: it’s there to make it clear to your browser that what it’s showing you really came from this blog, and not from some “man in the middle”. TLS isn’t there to encrypt, it’s there to authenticate. The encryption is there just because it’s part of the default settings.</p>

<p>That is likely true for most encryption today: the data on my laptop, my phone, my tablets, my S3 buckets, etc. is all encrypted. That means that to access any of it, I need the key – or at least I need access to the key. Without that, I don’t have access to any of it, and neither will you.</p>

<p>This fact is what the “bad guys” use when they attack you with ransomware: they encrypt your data, on your computer, so it’s no longer available <em>to you</em> until you pay the ransom. The fact that your laptop out-performs a vintage supercomputer helps them more than it helps you in this case. But the real pain comes from you no longer having the key to your data.</p>

<p>And that’s the drawback: sometimes you need to be able to access data – you need the data to be <em>available</em> to you, even if you don’t have the keys. You need to be able to read it, but not change it. You don’t need the <em>confidentiality</em> that comes from encryption, even if you do need the integrity you can validate with authentication. Sometimes, availability trumps confidentiality.</p>

<p>In Operational Technology, where physics meets software and cyber security takes on a meaning that is very different from Information Security, that “sometimes” becomes “almost always”. That is why DNP3 Secure Authentication version 6 encryption is still optional<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>. OT aside, the non-availability argument applies to anything that is encrypted: encryption comes at the cost of not just some compute cycles, but of availability as well. Sometimes, we ought to ask ourselves if it’s really worth it.</p>

<p>Sometimes, it isn’t.</p>

<p>So, how does one go about choosing what to encrypt and what to leave in the clear? The answer to that question is a technique called “Design Failure Mode and Effect Analysis” or DFMEA. A DFMEA allows you to look at your architecture using the perspective of how it might fail, and to plan for such failures. You can do this before you’ve implemented anything, or at any time after that, and you should probably do this on a regular basis. It’s really not that complicated provided you know the architecture you’re working with.</p>

<p><a href="/assets/2023/02/zta-app-diagram-for-dfmea.svg" target="_blank"><img src="/assets/2023/02/zta-app-diagram-for-dfmea.svg" width="200px" align="right" /></a>Let’s look at an example web applicaiton and analyse it. The application, which we’ll call “Crassula”, implements a web store for a florist. It uses a static website generated with Jekyll, and an Angular app embedded in that website for the purchasing workflow. The static front-end and angular app are served out of an S3 bucket behind a CloudFront proxy, the back-end for the store uses some serverless functions (lambdas), and S3 bucket to download invoices from, and a NoSQL database. The whole thing is tied together using AWS’ Simple Queue Service and deployed using CloudFormation.</p>

<p>The S3 bucket that contains the front-end (static site and Angular app) is the first point of entry for any customer. The Angular app calls out to the API and implements the workflows, including downloading invoices from the second S3 bucket using SAS URIs, but unlike the static app it can handle temporary failures of the API or the S3 bucket with a modicum of grace. That means that, for the front-end to work properly, the S3 bucket containing it needs high availability, but we don’t care as much about its confidentiality: we don’t need its contents to be encrypted and we can leverage a content delivery network like CloudFront to ensure availability.</p>

<p>On the other hand, access to invoices is a different matter: invoices contain confidential, personally-identifiable information that should not be available to anyone except the customer themselves and the store’s personnel. That means our DocumentDB should almost certainly be encrypted, as should the S3 bucket containing the invoice PDFs for download. Failure to access either of these two resources would have one of two possible effects in this application: failure to access the S3 bucket would thwart the delivery of an invoice to a customer, possibly delaying payment. In such cases, an alert to the shop’s staff would be warranted to mitigate the issue. Failure to access the database could either result in a transaction not completing, or in an invoice not being generated. In both cases, staff should be alerted but in the former case, any charge should also <em>not</em> be authorized. In any case, proper controls for failures of availability can be implemented here whereas for an inability to download the front-end application, detection of such a failure as well as its mitigation may require additional infrastructure and resources that would arguably be needed anyway, but not due to the confidentiality of the deployed front-end.</p>

<p>This is the essential trade-off: failure of an encryption mechanism or loss of the key leads to data not being available. In our example, the front-end is essentially guaranteed high availability through the content delivery network which cares much less about confidentiality than it does about availability. The back-end, on the other hand, makes the trade the other way around, though mitigations for failure (of availability) can be identified and implemented.</p>

<p>The way to identify those failures, and the way to identify how to mitigate and (as needed) resolve those failures, are are a different topic: <em>design failure more and effect analysis</em> or DFMEA. You can read about that <a href="https://rlc.vlinder.ca">on my other blog</a>.</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>I may have authored the proposal to put it in using an AEAD, but the DNP CSTF can attest that I’ve never argued for it to be the default option. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>rlc</name></author><summary type="html"><![CDATA[Encryption is the mechanism by which normal data, like this text, is made to look like random gibberish – arguably also like his text. The basic idea of encryption has been around ever since the first secret was confidentially told to someone in earshot of someone else: when the first “Alice” had a message for the first “Bob” that she didn’t want the first “Eve” to understand, even if she could overhear it.]]></summary></entry><entry><title type="html">Responsible disclosure</title><link href="https://applied-paranoia.com/2022/03/20/responsible-disclosure.html" rel="alternate" type="text/html" title="Responsible disclosure" /><published>2022-03-20T00:00:00+00:00</published><updated>2022-03-20T00:00:00+00:00</updated><id>https://applied-paranoia.com/2022/03/20/responsible-disclosure</id><content type="html" xml:base="https://applied-paranoia.com/2022/03/20/responsible-disclosure.html"><![CDATA[<p>When it comes to cybersecurity, surprises are generally not a good thing: they usually mean something is vulnerable, something went wrong, or something blew up (hopefully figuratively). They also <em>always</em> mean something needs to be done: some software or firmware needs to be updated, some configuration needs to be changed, operations are disrupted.</p>

<p>Responsible disclosure doesn’t take the surprise away completely, but softens the blow a bit. It basically means that when you find a vulnerability, especially if you find it in a popular piece of software, you don’t immediately post it to your blog or social media: you use the “proper channels” to tell whoever maintains the software about it, so they can fix it, communicate with their stakeholders, and make sure that when the vulnerability does become public knowledge, it’s already been fixed.
<!--more-->
Responsible disclosure is not easy: it puts a burden on whoever found the vulnerability (who ends up being called a “security researcher” no matter what they do in their day-to-day life) to find the “proper channel”, but it also puts a burden on whoever wrote the software in the first place to create that channel. Especially for small vendors, small teams, or open source software volunteers, that can be quite a burden.</p>

<h2 id="the-sorry-state-of-responsible-disclosure-in-canada">The sorry state of responsible disclosure in Canada</h2>
<p>Some countries, like the US, have government agencies to take on some of the burden of the security researchers. Canada has no such government organization, as <a href="https://www.cybersecurepolicy.ca/vulnerability-disclosure">shown by the Cybersecure Policy Exchange</a>, a cybersecurity lobbying organization funded by RBC and run from Ryerson University in Toronto. As they point out:</p>

<ul>
  <li>Canada does not have a “distinct and clear disclosure process for vulnerabilities involving government systems”</li>
  <li>Canada does not “[describe] the vulnerability submission and verification process”</li>
  <li>Canada does not “[provide] terms and rules for disclosures”</li>
  <li>Canada does not “publicly dessimate information about vulnerabilities disclosed through [a] coordinated process”</li>
  <li>Canada does not “publicly give acknowledgement or credit after disclosure”</li>
</ul>

<p>Our neighbour to the South, as well as the European Union, Japan, Russia, and Great Britain do all of these, and China does all of them except for public acknowledgement, according to the Cybersecure Policy Exchange.</p>

<p>It’s rather saddening to see that my adopted country is lagging behind, but this post is not about Canada.</p>

<p>A “distinct and clear disclosure process” would lighten the load on security researchers a bit: it allows them to contact a single (government) agency that can then coordinate with affected vendors, businesses and government services to ensure resolution of the issue and careful dissemination of relevant information. For example, the security researcher, or the affected vendor or open source project, does not have to try to find out who is affected by the vulnerability if there’s a government agency that is dedicated to doing that. Pro-actively reaching out to critical infrastructure providers (i.e. privately owned utilities for the power network, water and waste water, telephony, etc.) to get an inventory of potentially-vulnerable (that is: all) software used in critical systems or have them maintain such an inventory in case there’s a vulnerability to be dealt with is a job better suited for a government agency than for dozens of small teams.</p>

<p>So, absent government support, who does a security researcher contact?</p>

<h2 id="who-to-contact-for-responsible-disclosure">Who to contact for responsible disclosure</h2>
<h3 id="who-to-contact-in-businesses-small-medium-and-large">Who to contact in businesses, small, medium, and large</h3>
<p>Usually, the only point of contact an “outsider” has with a company is either through sales or support. Typically, neither sales nor support are trained to deal with cybersecurity issues, but they should at least know who to route the issue to. This is where things start getting complicated, though.</p>

<p>While large businesses may (and really should) have a dedicated cybersecurity team to help product teams coordinate their responses to vulnerabilities and incidents, those teams should provide guidance and support but will typically not know the ins and outs of the market a particular product team operates in: that’s Marketing’s job. That means that such dedicated teams will not necessarily know who uses the product and how it is used, and will not be able to analyse the issue to ascertain whether it is actually a cybersecurity issue in the product. It is up to the product teams to develop threat models, deployment models, and vulnerability management plans, provide their customers with guidelines on how to deploy the product, and show conformance to requirements assuming that those guidelines are followed, and ultimately to respond to any issues that may occur. I.e. dedicated cybersecurity teams need to adopt a “you can do it, we can help” attitude.</p>

<p>Smaller business don’t have dedicated teams for cybersecurity: such teams are expensive and, while they are worth the investment for larger businesses after some time (i.e. as cybersecurity incidents which would negatively affect the company’s reputation are avoided), smaller businesses simply don’t have the resources  to make that investment. They may, from time to time, be able to engage with a consultant to get training on how to develop threat models and deployment models, vulnerability management plans, what zero-trust architectures are, and what some relevant industry standards are to conform to, but such engagements do not change where the responsibility for the product and its security ultimately lies: with the product team. Such engagements also do not typically include rapid response to cybersecurity incidents, even when such services are available.</p>

<p>That means that responsible disclosure should go to the product team first, at which point a response team made up of marketing, R&amp;D, project management, and (internal if available, external otherwise) cybersecurity experts needs to come together and assess the issue, under the responsibility of the product team.</p>

<p>Customer support teams are often not trained for cybersecurity issues. In light of this, they need to be kept in the loop and, while they often won’t be included in the response team directly, they should at least be listened to and be allowed to voice their concerns and those of the customers they’re in contact with, and be informed on the progress of the analysis and the fix.</p>

<p>The support team will often be the best informed within the company as to how customers use the product, and may therefore be better informed than the development team to know how vulnerable systems really are. This can be an important input into the analysis, as the vulnerability may be one of deployment rather than the product itself.</p>

<h3 id="who-to-contact-in-open-source-projects">Who to contact in open source projects</h3>
<p>We’ll leave businesses aside for a moment and turn our attention to unfunded open source projects. There are thousands of these, maintained by unpaid volunteers who do this important work for the love of the challenge, the community, or whatever else drives them. I’ve contributed to a few open source projects myself: it’s fun and sometimes quite engaging, but I wouldn’t be able to express what drove me do to it.</p>

<p>From a cybersecurity perspective, though, it is hard to implement good security practices if the only resource you have is your own free time. GitHub and similar platforms provide some resources, such as automatic alerts to let you know you depend on something vulnerable, but it doesn’t “automagically” provide you with necessary project infrastructure and guidelines to build vulnerability management plans, threat models, etc. or to respond to emergent threats.</p>

<p>Most open source projects do not have documentation to tell you how to safely deploy them, do not register their users so they can be updated in case there’s a vulnerability, don’t distribute security bulletins, etc. The Apache Foundation provides some of that infrastructure and provides a framework to work in, but many other organizations do not, and most open source project are not part of any organization to start with.</p>

<p>So, for a run-of-the-mill open source project with no funding, no corporate sponsor, and no foundation to support it, who you gonna call? Usually, the developer.</p>

<h2 id="a-few-prerequisites-for-responsible-disclosure">A few prerequisites for responsible disclosure</h2>
<p>Regardless of who you end up contacting, here’s a few things that need to be true for any responsible disclosure:</p>

<ol>
  <li><strong>Disclosure needs to <em>not</em> be public</strong>, or at least not right away: the product team (or lonesome developer) needs to have time to prepare a response, figure out whether the vulnerability is legitimate, fix it if it is, update documentation, prepare a security bulletin, etc. A lot of work goes into responding to a vulnerability, so disclosing it too quickly will do more harm than good.</li>
  <li><strong>The product team (or lonesome developer) needs to be responsive</strong>: they need to quickly set up their response team, which needs to include someone who is in charge of coordinating the response, someone who can analyse and fix the underlying issue, and someone who knows who the stakeholders are and can coordinate with them. For small businesses and open source projects, this may well all be the same person (who is in for a pretty stressful few days/weeks in that case), but for larger businesses it really shouldn’t be.</li>
  <li><strong>Do not acknowledge a security issue without input from the development team</strong>: they need to analyse the issue first, and confirm that it’s a cybersecurity issue (and not just a run-of-the-mill bug). This is especially important for support teams: if someone calls support about an issue, acknowledging it as a cybersecurity issue may set of vulnerability management plans at the customer which may not have an “exit clause” in case the issue was not a vulnerability to begin with.</li>
</ol>

<h2 id="difficulties-with-responsible-disclosure-in-open-source-projects">Difficulties with responsible disclosure in open source projects</h2>
<p>Aside from the difficulties already mentioned above, open source projects have some extra difficulties that commercial software does not when it comes to responsible disclosure. These may be summarized as “everything is out there”.</p>

<p>If there is any kind of team or community to speak of for the project, any issues or changes to the software will generally be discussed on a mailing list that will often be public. Such mailing lists may be monitored by so-called “black hat hackers”, especially if the software is popular. Keeping vulnerabilities off that public forum is often more difficult than it may seem, as that requires the developers of the project to have some other, non-public means of communication.</p>

<p>Even if that is the case, a fix will eventually be published in source code, by being pushed to a git repo or even just published as a tarball. Most open source projects publish source code, and may or may not publish binaries. Again, it is easy to monitor such releases and black hat hackers generally will do just that. While the commit message may not refer to a CVE number directly, it will still generally describe something like “fix a race condition that could occur if …”, which may be enough to understand that a vulnerability is being addressed. Such changes may fix the issue, but will also expose it to knowledgeable onlookers, who may then exploit deployments that haven’t been updated yet.</p>

<p>Once the fix has been published, open source projects don’t necessarily know who uses their software, so it becomes difficult to discreetly contact stakeholders to have them update their software without making the vulnerability public. That means that stakeholders have no choice but to learn about the vulnerability at the same time (or after) the “bad guys” do as well.</p>]]></content><author><name>rlc</name></author><summary type="html"><![CDATA[When it comes to cybersecurity, surprises are generally not a good thing: they usually mean something is vulnerable, something went wrong, or something blew up (hopefully figuratively). They also always mean something needs to be done: some software or firmware needs to be updated, some configuration needs to be changed, operations are disrupted.]]></summary></entry><entry><title type="html">Getting rid of Java - without getting rid of Java code</title><link href="https://applied-paranoia.com/2021/12/29/getting-rid-of-java.html" rel="alternate" type="text/html" title="Getting rid of Java - without getting rid of Java code" /><published>2021-12-29T00:00:00+00:00</published><updated>2021-12-29T00:00:00+00:00</updated><id>https://applied-paranoia.com/2021/12/29/getting-rid-of-java</id><content type="html" xml:base="https://applied-paranoia.com/2021/12/29/getting-rid-of-java.html"><![CDATA[<p>In my <a href="https://applied-paranoia.com/2021/12/23/log4j.html">previous post here on applied-paranoia.com</a>, I concluded that one of the lessons learned from the log4j debacle was that we should get rid of public-facing interfaces (UI and API) based on Java technologies. How do we do that, without also getting rid of the thousands upon thousands of lines of Java code, IP, and “sunk” investment? Clearly, just deleting the code and re-writing it all in some other language is not an option!
<!--more--></p>

<p>Let’s start with some low-hanging fruit: <strong>don’t start any new Java projects</strong>. Java developers can be converted to other, similar languages such as C# with fairly minimal effort and contrary to common belief, C# is just as portable as Java for the vast majority of use-cases. C# also doesn’t have some of the cybersecurity issues that Java has been plagued with over the last few decades, in large part because it has had the opportunity to learn from Java, and from Windows, in that respect. If there is a choice for programming languages, <em>Java should only be used if no other options are available</em> for new projects.</p>

<p>But let’s face it: how often do you really start something new? In the vast majority of cases, the “something new” builds on code you already have, in a code base that may be years or decades old. If you have Java-based IP, you will be writing Java code for a very long time to come.</p>

<p>So, how do you go about securing those services and applications, despite them being written in Java?</p>

<h2 id="the-issues">The issues</h2>
<p>There are two major issues to be concerned with:</p>

<ol>
  <li>The JVM may “reach out” and connect to third-party servers</li>
  <li>The JVM may make changes to your filesystem</li>
</ol>

<p>There are several other issues that you may be concerned about, but that are not specific to the JVM:</p>

<h3 id="the-jvm-reaching-out-to-third-party-servers">The JVM reaching out to third party servers</h3>
<p>The main issue with log4j is that it could be coerced into using JDNI to download and execute code from a third-party server. Even if the code wasn’t executed, because the (unauthenticated) user had full control over the URI used to fetch the code and could use environment variables from the server as well, data from the environment could be leaked by the same occasion even if foreign code execution was turned off.</p>

<p>TLS is usually the go-to technology to secure connections between servers, and it does authenticate the server you’re connecting to, but all <em>that</em> does in this case is tell you that the server at very.evil.address really is at very.evil.address. It provides authentication and encryption, but it doesn’t tell you whether you can trust the resources you’re downloading, especially if they are code.</p>

<p>On the other hand, shutting off execution of downloadable code may break your application if it had a legitimate use for such a pattern. Remember Java is a pre-REST technology and many applications from the late 1990s are still used today. There’s a reason why similar technologies are still in every-day use. <em>Should</em> they be put behind a RESTful interface in stead? Probably, yes. But that can be a lot of work.</p>

<p>So to resolve the issue of the JVM reaching out to third-party servers, without breaking the the applications we’re trying to secure, can’t just be a matter of “no longer doing that”.</p>

<h3 id="the-jvm-making-changes-to-the-filesystem">The JVM making changes to the filesystem</h3>
<p>Once someone has been able to exploit a vulnerability, they need to “make it stick” if they want to be able to take control of the server (e.g. to enrol it in a botnet) later. Direct ways of doing that would include vulnerabilities that can upload arbitrary files and put them in a pre-set location (such as <a href="https://nvd.nist.gov/vuln/detail/CVE-2016-3088">CVE-2016-3088</a>), but if you can get the JVM to execute arbitrary code (which most of the recent vulnerabilities appear to be about), you can certainly have that arbitrary code install malware as well.</p>

<p>Changing things in the filesystem is not a bad thing per se: there are many legitimate reasons to do that that have no bearing whatsoever on cybersecurity. It is not something you want to disallow outright, nor something you usually <em>can</em> disallow outright without breaking applications. Of course, you could use databases (SQL or NoSQL alike) to  manage your non-volatile state in stead, and in most cases, you should.</p>

<h2 id="approaches-to-a-solution">Approaches to a solution</h2>
<p>The issues above can be addressed with a number of common tools. These tools may require a bit of extra infrastucture to set up and manage, but most of that can be done using IAAS services which can, in turn, be deployed using an Infrastructure-As-Code approach. I won’t go into to the details of setting up and deploying the required infrastructure below, but I will provide a bit of an overview of each of the tools I propose to use as countermeasures to Java’s two main issues, and how they help overcome them.</p>

<h3 id="sandboxing">Sandboxing</h3>
<p>One advantage of using a virtual machine is that it can be used to “sandbox” an application: the resources the application gets access to are limited to the ones available within the sandbox. Communications with a sandboxed application is always an issue, though.</p>

<p>The JVM does this as well: you can limit the amount of memory, CPUs, etc. the JVM can use for any particular application, and you can tell it where to find the classes it needs. This actually used to be an argument <em>for</em> using Java. The whole issue we’re discussing here, though, is that the JVM itself cannot be trusted. The first thing to do, therefore, is to sandbox the JVM.</p>

<p>In general, the easiest way to do this for a monolithic application (like most decades-old Java applications will tend to be), would be to wrap the whole thing in a VM of which the entire contents is controlled. This approach also works with more modern Java applications, which may be less monolithic in nature.</p>

<p>The question then becomes how to control the entire environment within a VM. The answer to that one is actually fairly simple: using an approach that allows you to write, as a single file or a small set of files, the procedure to set up your VM. These files should not end up in the VM itself, or should at least be deleted before the VM is deployed, but they can be versioned and tested.</p>

<p>Now, forget for a moment that I said “VM” in the last two paragraphs.</p>

<p>The “procedure to set up your VM” should of course be a <code>Dockerfile</code>. The VM itself could be either a pod in a Kubernetes cluster or just a Docker container running by itself. It’s not all that important which one it is, as long as you have a Docker container that contains only what your application needs to run.</p>

<p>Once you have that, you can (and should) set up a mechanism to <em>frequently</em> update your image (by re-running Docker and making sure your <code>Dockerfile</code> updates the image when run) so at least the underlying “VM” stays up to date. You could make this part of your CI/CD pipeline.</p>

<h3 id="limiting-filesystem-access">Limiting filesystem access</h3>
<p>Even if you use either disposable VMs or Docker containers to host your Java applications in, you should still limit the JVM’s access to the filesystem as much as possible: run the JVM with the least privileges required (even inside the Docker container) and if you mount volumes into the Docker container or disposable VM, make sure you restrict the mount options as much as possible.</p>

<p>For example: most host systems will allow you to strip the “executable” bit from files written to the filesystem and will allow you to filter it out when read. This is a security measure that has been available for decades now, and is still useful today.</p>

<p>If you have applications that communicate with each other through shared files, make sure that once the file is written and published to the other application (or step in the pipeline), it can no longer be altered. It should effectively disappear from view for the publisher before the subscriber can access it. If you don’t, you’ll have a race condition that may become exploitable.</p>

<p>Configuration and code files should generally be read-only to the application, so <em>make it so</em>: if you have the code of the application, make sure it doesn’t need anything other than read-only access to those files.</p>

<p>The less your (untrusted) application can do, the less likely its capabilities will be exploited.</p>

<h3 id="code-signing">Code signing</h3>
<p>Java has implemented optional code signing and signature verification for a while now. It requires the application to be distributed in a JAR file (which is very common) and requires that JAR file to be signed using (the private key corresponding to) a trusted certificate. Once that has been put in place, attempts to execute classes outside of appropriatey signed JARs will fail with an access error.</p>

<p>This is a simple, though somewhat arduous, way to make sure only code that you actually trust is executed: you need to make sure that all the JARs that contain such code are signed, that all the code you need is in those JARs, and that you require signature verification.</p>

<p>For this to <em>actually</em> work, though, you need a bit more than just signing all your JARs (which you can do as part of your CI/CD pipeline, provided you run that on a trusted infrastructure):</p>

<ol>
  <li>you need to set up a PKI, including expiry policies and the like</li>
  <li>you need to audit <em>all</em> of your dependencies for trust</li>
  <li>you need to re-package your application, using only those trusted dependencies, into signed JARs</li>
  <li>you need to reconfigure your deployment to only run signed JARs</li>
  <li>you need to version-control that deployment configuration</li>
  <li>you need to test the deployment configuration (preferably regularly) for its inability to run badly signed, or unsigned, JARs</li>
  <li>you need to make sure you re-deploy your entire application before the certificates you use to sign them expire (so likely at least once a year)</li>
</ol>

<p>Some of these, like setting up your PKI, would be worth their own post, but I’ll just go through each in a single paragraph to explain what I mean and why it’s important.</p>

<p>Setting up a PKI is the first step: you need to be sure that what you trust is indicated as such, and to do that you need to authenticate it. Setting up a PKI does not have to be expensive, but you do need to know what you’re doing and you need to keep your root CA certificate safe. In this case, you <em>should not</em> use a commercial CA unless you root your trust in an intermediate CA certificate that you create yourself: using a commercial CA means that anyone who can pay the fee can get a trusted certificate – not what you want. There are quite a few caveats to consider here, but without a PKI you can’t implement code signing effectively. It’s just that simple.</p>

<p>Once you have a PKI in place you need to be sure about what you’re signing, which means auditing the code. Some of that can be done automatically: you can, for example, use a static analyzer to analyze all the third-party open source code. If you fix the issues you find (which you generally should) you should contribute the fixes back upstream: it’s the right thing to do as a netizen, and it means you don’t have to maintain the patches afterwards. Remember to use responsible disclosure if you find security issues in third party code.</p>

<p>Once you’ve audited your application, you should re-package it into signed JAR files, using only the trusted dependencies that you have now audited. That’s just how code signing works in Java.</p>

<p>Once you’ve done that, you need to configure your deployment to run only signed JAR files. There’s plenty of documentation on how to do this, so it shouldn’t be too hard, dependending on how your deployment works. This should be part of your CI/CD as well.</p>

<p>Make sure the deployment configuration is version controlled: you want to make sure you can audit any changes that were made to the configuration, and you want to make sure that, just like your code should have, you have a review and approval process in place that explicitly looks for cybersecurity issues. For any change made to the configuration, you need to be able to show why it was done, when it was done, by whom it was done, and what the change was.</p>

<p>Also make sure you (periodically) test that you really can’t run unsigned JARs or JARs signed using some other CA: a large part of your security depends on this, so the risk associated with failure is high (the likelihood of things going wrong is high, and so is the impact).</p>

<p>Finally, depending on your PKI expiry policies, you will need to re-deploy the entire application on a regular basis to refresh the certificates being used. This generally means that if you redeploy your application anyway, say on a monthly basis, you don’t have anything special to do other than monitor the age of your oldest deployment. Make sure this works, because your application <em>will</em> stop working if you forget!</p>

<h3 id="proxy">Proxy</h3>
<p>To prevent the JVM from unexpectedly reaching out to the internet, the application should be behind a proxy.</p>

<p>Proxies are what IT in big companies use to eavesdrop on their staff’s use of the Internet. Like you shouldn’t trust the JVM, IT doesn’t trust non-IT staff, so internet usage is monitored for anything illegal, inappropriate, or unsafe for company IP and infrastructure.</p>

<p>While we don’t expect the JVM to download videos of cute cats or to spontaneously buy illegal firearms, we do need to make sure that it doesn’t connect to any servers on the Internet that aren’t on a “white list”, and you’ll want to scan the contents of the messages being exchanged.</p>

<p>Being able to scan the messages being exchanged usually means the proxy will “spoof” the external site’s certificate with its own, which, again, means you need a PKI. Note that this implies that the proxy implements its own CA. It also means its CA certificates <em>should not</em> be signed by a commercial trusted root (because doing that would be <strong><em>very</em></strong> dangerous) and should not be trusted anywhere outside of your organization.</p>

<p>The proxy should basically, at the very least, check that the server being reached out to is on the white list, create a certificate to spoof the target, reach out to the server itself, validate then relay the request, validate then relay the response, and keep validating and relaying requests and responses as long as it passes muster.</p>

<p>Obviously, if your application has no legitimate reason to reach out to the Internet, you don’t need a proxy: you just need a firewall (which you need anyway).</p>

<h3 id="api-gateways-and-deep-firewalls">API Gateways and deep firewalls</h3>
<p>Outgoing traffic is not the only problem, though: ultimately, the issue is that someone from the outside is trying to coerce your application into doing something you don’t want it to do. It would be a pretty odd approach to lock your application up in a sandbox without even looking at what’s going into the application.</p>

<p>An API Gateway does just that. It’s usually used for three things: load balancing, HTTPS-to-HTTP forwarding (so your application code doesn’t have to deal with HTTPS), and deep inspection. We’re interested in the deep inspection in this case.</p>

<p>OWASP, the Open Web Application Security Project, has a set of “core rules” that you can find at <a href="https://coreruleset.org/">coreruleset.org</a>. Implement them. Use ModSecurity if you must (but not it will no longer have commercial support in 2024).</p>

<p>This is probably the hardest part (this and the proxy, really): it will cost the most to implement and maintain, but it is also your first line of defense.</p>]]></content><author><name>rlc</name></author><summary type="html"><![CDATA[In my previous post here on applied-paranoia.com, I concluded that one of the lessons learned from the log4j debacle was that we should get rid of public-facing interfaces (UI and API) based on Java technologies. How do we do that, without also getting rid of the thousands upon thousands of lines of Java code, IP, and “sunk” investment? Clearly, just deleting the code and re-writing it all in some other language is not an option!]]></summary></entry></feed>