More from Ian's Blog
On March 30th 2016 the first commit of TLS Inspector, back then called Certificate Inspector, was made - and just shy of two weeks later version 1.0 was released. Now, 10 years later, TLS Inspector celebrates its 10th year, and I've got some exciting changes to share with you all. A Not-So-Quick History The original idea for TLS Inspector came about when I was travelling on a passenger ferry between Vancouver Island and mainland Vancouver. The operator, BC Ferries, provided free WiFi at the time, which was useful as cell service is spotty out in the water. However, this free WiFi wasn't working right, as all TLS Connections were failing with untrusted certificates (even though no captive portal was being presented). I looked in the App Store for any app that could help see what was going on, and found nothing but a one, very limited app filled with in-app-purchases. "I could probably write something like this myself", I thought, and so began TLS Inspector. In 2017 the app caught the attention of cryptologist Kenn White, who reached out to me and provided invaluable feedback. After a short and intensive development cycle, TLS Inspector graduated from a small proof-of-concept into the tool it is today, with release 1.5 being the first where the app gained widespread use. Originally named "Certificate Inspector", the app was renamed to "TLS Inspector" after DigiCert issued a cease-and-desist over the use of the name. In many ways, I felt that this was a good thing in the end, as the new name better reflects what the app is truly about - inspecting the entire TLS connection - not just certificates. Modernization In August 2023 I began work on implementing support for DNS over HTTPS within the app, however this feature never came to realization. Instead, my attention shifted into focusing this new-found DNS energy towards a new app, DNS Inspector. Even though TLS Inspector never got this feature, I did gain one piece of valuable insight from the effort; the realization that TLS Inspector's codebase was quickly aging, and was at serious risk of being left behind. When TLS Inspector was first released, Swift was still a very new language, and those familiar with Swift's early days will recount the growing pains it had, where every year there were major breaking changes to the language and APIs. In addition, Swift did not offer functional interoperability with C for many years - this made using Swift in TLS Inspector anywhere from impossible to incredibly difficult. As a result, Objective-C was used. However by 2019 Swift has significantly matured, and so I was comfortable splitting the app into two components - a Swift-based UI and an Objective-C based backend. Despite its maturity, its C interoperability was still very new and, frankly, very bad, so this design made sense. Apple has been investing an immense amount of time and effort into evolving and growing Swift, and they could not be more clear that the future of Apple Development is exclusively on Swift - SwiftUI is a great example of this. DNS Inspector was my first official SwiftUI app, and served as a validation test for both me and the platform, to answer the question of "do I feel it's ready?", and the answer was yes. Today we finally mark the completion of our transition to Swift, as TLS Inspector 3.0 written 100% in Swift, including all interaction with C libraries. Three-Point-O Let's get one thing clear right away: the experience you've come to expect from TLS Inspector is not changing. This effort to completely rewrite the app was also done with a strong desire to not change how the app worked. I, like many of you, dislike when a tool you may rely on suddenly changes how it works. Tradespeople don't just wake up one morning to suddenly find that their hammer had a software update and now it works entirely differently, why should TLS Inspector? I'm making a point of stressing this because I'm being quite serious when I say that this update is a total rewrite of the application. Nearly every component you see below has been substantially to entirely rewritten. And no, before you ask, the phrase "biting off more than you can chew" means nothing to me. TLSUI Before I get into talking about what changes I made, I want to clarify what I mean when I talk about things like SwiftUI or a "Swift-based user interface", because those sound like the same thing. So, buckle up, because it's time to learn about UIKit. Ask anyone who'd developed a native desktop application for macOS (no, not Electron) and they will absolutely gush about how much they love AppKit, and you can count me among one of those people! When Apple was building iPhoneOS (which later became iOS), they naturally wanted to reuse what they could from OS X, AppKit being no exception. However, as countless designers will tell you, user interfaces designed for touch screens absolutely do not work for traditional keyboard-and-mouse PCs. Apple knew this, and forked AppKit over to UIKit so the two could diverge cleanly. UIKit was, and I suppose still technically is, how you make native user interfaces on iOS apps. Apple provides you with three ways to work with UIKit: Do it entirely in code by yourself Use Xcode Interface Builder (.nib and .xib) Use Storyboards Storyboards are just a fancy way of joining together xibs, so realistically 2 and 3 are the same thing. When referring to the "Swift-based User Interface" of TLS Inspector before version 3.0, I'm referring to option #3 above. TLS Inspector previously used storyboards with UIKit for the entire UI. Initially being written in Objective-C and later ported over to use Swift. SwiftUI, however, takes a different approach. SwiftUI is Apple's attempt at modernizing the user interface tooling provided to developers by dropping interface builders and storyboards, and going all-in on declarative interfaces. As mentioned earlier, DNS Inspector was a test for me to see if I felt that SwiftUI was ready for da big leagues (as we say in the biz). The answer was a confident mostly sorta kinda yes. It's very clear that Apple is pouring a lot of resources into building out SwiftUI, and with every iOS release it gets better! The problem is that with every iOS release Apple changes things, and I hate when they do that! Unlike when Apple releases a new version of The Swift Language, SwiftUI is a runtime framework, which makes supporting old iOS versions quite annoying. Although, to be fair, I'm sure people found AppKit annoying during its early years. I only hopped on the UIKit train after it had almost 10 years to mature. SwiftUI has a lot of extremes. On one hand, it makes creating List-based interfaces (which used UITableView before) such a breeze, but at the same exact time there's bizarre limitations where your best bet is to just abandon SwiftUI and fall back to UIKit elements. Anyways, enough history and preamble, let's finally talk about the new user interface: As I said about 8 billion words ago, the goal here was not to dramatically change TLS Inspector - just to give it a gussy up, at most. The Main View The main view of the app has been tidied up, primarily by removing the Tip section. Let's be real, you don't need to be reminded that you can inspect websites from other apps every single time you use TLS Inspector. The advanced inspection parameters can be accessed by tapping on the gear (or cog, if you speak metric). Response View The response views are largely unchanged aside from adding more information. The app now highlights the root certificate in the chain as well as performs a reverse DNS lookup on the remote address. The trust details view has been tidied up to provide more direct information about the trust situation. Additionally, specific trust failures will also have what I call "Safety Warnings", which highlight when common indicators-of-compromise have been detected, directing users to engage with Apple support or a trusted friend. A "quick exit" button will immediately exit the app. Certificate Information The certificate information screen has been somewhat reorganized, moving the alternate names right to the top, and improving support for new "subject-less" certificates such as those issued by Let's Encrypt. In addition, there's several new datapoints provided: The results from CRL or OCSP checks are now displayed The source of the certificate is displayed (if the certificate was sent by the server or found on the device) All X.509 extensions are displayed, even those not understood by the app TLSKit TLSKit is the name of the "Backend" of the app - this is a Swift Package that performs all of the inspection tasks, collects and presents the results. Previously TLS Inspector used CertificateKit, which was a classic Objective-C based framework for accomplishing the same goals. However, CertificateKit contained some of the oldest and (in my opinion) worst code of the app. It was a frequent source of crashes and locked me out from enjoying new features with Swift Packages since it used legacy code and legacy tools. TLSKit is more than just a Swfit-rewrite of CertificateKit, however, because I took this as a perfect oppertunity to go back to the drawing board and rethink how the backend should work given how the app works today. Remember, CertificateKit contained some of the oldest code, back from before I really knew what I was doing or what I wanted TLS Inspector to be. External Dependencies A major hurdle that proved as a total "Go/No Go" test for this rewrite was the ability to use external dependencies in a Swift package, namely Curl and OpenSSL. After doing some initial research it appeared as if Apple had finally added proper Foreign function interface (FFI) support in Swift. This means one could directly invoke C function calls in these libraries within Swift without the need for an Objective-C bridge. However, in my effort to utilize Swift Packages, I needed to find a way to link to precompiled static libraries. To solve this puzzle, I reached out on the Apple Support forums, and with help from the fine folks there we were able to untangle this mess and get things working. An exciting prospect I plan to work on after 3.0 has stablized is moving away from OpenSSL towards using WolfSSL. This will also unlock the ability to inspect QUIC connections, something not currently possible in the app. It Takes A Village To say this was a massive undertaking for just one guy who does this part time is an understatement, and it's clear that I never could have done this alone. I cannot wrap things up here without giving my sincerest of thank you to the following: David (aka Catfish_Man), for their incredible patience and guidance helping me with the Swift 6 transition. Quinn (aka "The Eskimo"), for repeatedly helping me get through difficult tooling challenges on the Apple developer forums. All of the people who helped test the app. You helped me catch some real nasty bugs early and provided such great feedback. ChatGP- nah just kidding. could you imagine though? lol.
Turns out buying Air Pods from eBay is risky, who knew? Well, clearly I didn't because I totally fell for one. I was looking to pick up some used AirPods Pro 2 to use while out on walks or whenever my usual over-ears wouldn't be comfortable. I found a decent-enough looking listing that was selling a "opened and used once, but otherwise new" set for only 30% off what Apple is asking. Seems fair, right? Wrong. What arrived in my postbox were counterfeits, and I'm left out of pocket (at least, until eBay processes my refund request). In the meantime, I went and did what I should've done from the start and just bought a genuine pair from a retailer, partly because I still actually want a pair, but now also because I want to compare the two side-by-side and show you all the differences. The Packaging The front of the counterfeit box is actually very well done. The printing is close enough to the real one that without a genuine to compare against you'd have a really hard time telling if its fake. The box is even embossed correctly. Remember that these were sold to me as having been opened, so the fact the box is a little smushed doesn't really mean anything. The rear of the boxes is where we start to begin to see differences, although nothing immediately telling of a fake without a genuine box to compare against. It's interesting that the fake box does not show the USB-C cable like the genuine one does. Note: I had already unsealed the fake box. The box arrived sealed and in plastic shrink-wrap. The genuine box did not come in plastic wrap. The bottoms of the boxes begin to show some of the key differences between a real and fake box. On the real box, Apple has printed all information to the box itself, including the serial number. The fake box uses stickers for everything. The contents of the box don't give away too many clues. I had unwrapped the included charging cable myself for reasons you'll see later, it was wrapped when I first opened it. The bud bits additional tips contain a few clues, notably the worse printing the size labels and the lack of the indentions in the packaging. The Charging Case The exterior of the case reveals no clues. The only visual difference is the difference in assembly location (which I wouldn't be comfortable saying identifies it as a fake), and the different typeface used for the labels. This is very hard to identify without a known-good comparison. However, while impossible to demonstrate with pictures, the feel of the fake case is notably worse than the real one. The fake case rattles when you shake it and sounds hollow when you tap it. The real one feels dense with no rattles. Inside the case looking at the serials we start to see some more key differences. While my phone's camera is doing no favours to the image, the printing quality on the fake pods is notably worse. In addition, note the lack of input and output power requirements The Individual Pods This is where the counterfeits really begin to standout. Whoever made this case did a really good job, but whoever did the pods did not. Genuine AirPods have vents on and around the body. These aren't just for style, and are actual grills. Fake AirPods, like mine, just use styled plastic fillers. In addition, the pre-installed tip should include its size embossed into the rubber Lastly, a very important thing to check, the serial number of each individual pod should be unique - it should not match the serial of the charging case. These are three individual products each with their own serials. Apple should label all 3 on the box, instead the only label the charging case. The Hard Evidence Taking the serial number of the charging case, M6WYX2HJKR, and putting it into Apple's warranty check tool, I am presented with irrefutable evidence of the forgery: USB-C, you say?
Securing My Web Infrastructure A few months ago, I very briefly mentioned that I've migrated all my web infrastructure off Cloudflare, as well as having built a custom web service to host it all. I call this new web service WebCentral and I'd like to talk about some of the steps I've taken and lessons I've learned about how I secure my infrastructure. Building a Threat Model Before you can work to secure any service, you need to understand what your threat model is. This sounds more complicated than it really is; all you must do is consider what your risks how, how likely those risks are to be realized, and what the potential damage or impact those risks could have. My websites don't store or process any user data, so I'm not terribly concerned about exfiltration, instead my primary risks are unauthorized access to the server, exploitation of my code, and denial of service. Although the risks of denial of service are self-explanatory, the primary risk I see needing to protect against is malicious code running on the machine. Malicious actors are always looking for places to run their cryptocurrency miners or spam botnets, and falling victim to that is simply out of the question. While I can do my best to try and ensure I'm writing secure code, there's always going to be the possibility that I or someone else makes a mistake that turns into an exploitable weakness. Therefore, my focus is on minimizing the potential impact should this occur. VPS Security The server that powers the very blog you're reading is a VPS, virtual private server, hosted by Azure. A VPS is just a fancy way to say a virtual machine that you have mostly total control over. A secure web service must start with a secure server hosting it, so let's go into detail about all the steps I take to keep the server safe. Network Security Minimizing the internet-facing exposure is critical for any VPS and can be one of the most effective ways to keep a machine safe. My rule is simple, no open ports other than what is required for user traffic. In effect this only means one thing: I cannot expose SSH to the internet. Doing so protects me against a wide range of threats and also reduces the impact from scanners (more on them later). While Azure itself offers several of ways to interact with a running VPS, I've chosen to disable most of those features and instead rely on my own. I personally need to be able to access the machine over SSH, however, so how do I do that if SSH is blocked? I use a VPN. On my home network is a WireGuard VPN server as well as a Dynamic DNS setup to work-around my rotating residential IP address. The VM will try to connect to the WireGuard VPN on my home network and establish a private tunnel between them. Since the VM is the one initiating the connection (acting as a client) no port must be exposed. With this configuration I can effortlessly access and manage the machine without needing to expose SSH to the internet. I'm also experimenting with, but have not yet fully rolled out, an outbound firewall. Outbound firewalls are far, far more difficult to set up than inbound because you must first have a very good understanding of what and where your machine talks to. OS-Level Security Although the internet footprint of my VPS is restricted to only HTTP and HTTPS, I still must face the risk of someone exploiting a vulnerability in my code. I've taken a few steps to help minimize the impact from a compromise to my web application's security. Automatic Updates First is some of the most basic things everyone should be doing, automatic updates & reboots. Every day I download and install any updates and restart the VPS if needed. All of this is trivially easy with a cron job and built-in tooling. I use this script that runs using a cron job: #!/bin/bash # Check for updates dnf check-update > /dev/null if [[ $? == 0 ]]; then # Nothing to update exit 0 fi # Install updates dnf -y update # Check if need to reboot dnf needs-restarting -r if [[ $? == 1 ]]; then reboot fi Low-Privileged Accounts Second, the actual process serving traffic does not run as root, instead it runs as a dedicated service user without a shell and without sudo permission. Doing this limits the abilities of what an attacker might be able to do, should they somehow have the ability to execute shell code on the machine. A challenge with using non-root users for web services is a specific security restriction enforced by Linux: only the root user can bind to port at or below 1024. Thankfully, however, SystemD services can be granted additional capabilities, one of which is the capability to bind to privileged ports. A single line in the service file is all it takes to overcome this challenge. Filesystem Isolation Lastly, the process also uses a virtualized root filesystem, a process known as chroot(). Chrooting is a method where the Linux kernel effectively lies to the process about where the root of the filesystem is by prepending a path to every call to access the filesystem. To chroot a process, you provide a directory that will act as the filesystem root for that process, meaning if the process were to try and list of contents of the root (/), they'd instead be listing the contents of the directory you specified. When configured properly, this has the effect of an filesystem allowlist - the process is only allowed to access data in the filesystem that you have specifically granted for it, and all of this without complicated permissions. It's important to note, however, that chrooting is often misunderstood as a more involved security control, because it's often incorrectly called a "jail" - referring to BSD's jails. Chrooting a process only isolates the filesystem from the process, but nothing else. In my specific use case it serves as an added layer of protection to guard against simple path transversal bugs. If an attacker were somehow able to trick the server into serving a sensitive file like /etc/passwd, it would fail because that file doesn't exist as far as the process knows. For those wondering, my SystemD service file looks like this: [Unit] Description=webcentral After=syslog.target After=network.target [Service] # I utilize systemd's heartbeat feature, sd-notify Type=notify NotifyAccess=main WatchdogSec=5 # This is the directory that serves as the virtual root for the process RootDirectory=/opt/webcentral/root # The working directory for the process, this is automatically mapped to the # virtual root so while the process sees this path, in actuality it would be # /opt/webcentral/root/opt/webcentral WorkingDirectory=/opt/webcentral # Additional directories to pass through to the process BindReadOnlyPaths=/etc/letsencrypt # Remember all of the paths here are being mapped to the virtual root ExecStart=/opt/webcentral/live/webcentral -d /opt/webcentral/data --production ExecReload=/bin/kill -USR2 "$MAINPID" TimeoutSec=5000 Restart=on-failure # The low-privilege service user to run the process as User=webcentral Group=webcentral # The additional capability to allow this process to bind to privileged ports CapabilityBoundingSet=CAP_NET_BIND_SERVICE [Install] WantedBy=default.target To quickly summarize: Remote Access (SSH) is blocked from the internet, a VPN must be used to access the VM, updates are automatically installed on the VM, the web process itself runs as a low-privileged service account, and the same process is chroot()-ed to shield the VMs filesystem. Service Availability Now it's time to shift focus away from the VPS to the application itself. One of, if not the, biggest benefits of running my own entire web server means that I can deeply integrate security controls how I best see fit. For this, I focus on detection and rejection of malicious clients. Being on the internet means you will be constantly exposed to malicious traffic - it's just a fact of life. The overwhelming majority of this traffic is just scanners, people going over every available IP address and looking widely known and exploitable vulnerabilities, things like leaving credentials out in the open or web shells. Generally, these scanners are one and done - you'll see a small handful of requests from a single address and then never again. I find that trying to block or prevent these scanners is a bit of a fool's errand, however by tracking these scanners over time I can begin to identify patterns to proactively block them early, saving resources. Why this matters is not because of the one-and-done scanners, but instead the malicious ones, the ones that don't just send a handful of requests - they send hundreds, if not thousands, all at once. These scanners risk degrading the service for others by occupying server resources that would better be used for legitimate visitors. To detect malicious hosts, I employ some basic heuristic by focusing on the headers sent by the client, and the paths they're trying to access. Banned Paths Having collected months of data from the traffic I served, I was able to identify some of the most common paths these scanners are looking for. One of the more common treds I see if scanning for weak and vulnerable WordPress configurations. WordPress is an incredibly common content management platform, which also makes it a prime target for attackers. Since I don't use WordPress (and perhaps you shouldn't either...) this made it a good candidate for scanner tracking. Therefore, any request where the path contains any of: "wp-admin", "wp-content", "wp-includes", or "xmlrpc.php" are flagged as malicious and recorded. User Agents The User Agent header is data sent by your web browser to the server that provides a vague description of the browser and the device it's running on. For example, my user agent when I wrote this post is: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:128.0) Gecko/20100101 Firefox/128.0 All this really tells the server is that I'm on a Mac running macOS 15 and using Firefox 128. One of the most effective measures I've found to block malicious traffic early is to do some very basic filtering by user agent. The simplest and most effective measure thus far has been to block requests that have no user agent header. I also have a growing list of bogus user agent values, where the header looks valid - but if you check the version numbers of the system or browser, nothing lines up. IP Firewall When clients start getting a bit too rowdy, they get put into the naughty corner temporarily blocked from connecting. Blocked connections happen during the TCP handshake, saving resources as we skip the TLS negotiation. Addresses are blocked 24 hours, and I found this time to be perfectly adequate as most clients quickly give up and move on. ASN Blocks In some extreme situations, it's necessary to block entire services and all of their addresses from accessing my server. This happens when a network provider, such as an ISP, VPN, or cloud provider, fails to do their job in preventing abuse of their services and malicious find home there. Cloud providers have a responsibility to ensure that if a malicious customer is using their service, they would terminate their accounts and stop providing their services. For the most part, these cloud providers do a decent enough job at that. Some providers, however, don't care - at all - and quickly become popular amongst malicious actors. Cloudflare and Alibaba are two great examples. Because of the sheer volume of malicious traffic and total lack of valid user traffic, I block all of Cloudflare and Alibaba's address space. Specifically, I block AS13335 and AS45102. Putting It All Together Summarized, this is the path a request takes when connecting to my server: Upon recieving a TCP connection, the IP address of the client is checked if it's either in a blocked ASN or is individually blocked. If so, the request is quickly rejected. Otherwise, TLS is negotiated, allowing the server to see the details of the actual HTTP request. We then check if the request is for a banned path, or has a banned user agent, if so the IP is blocked for 24 hours and the request is rejected, otherwise the request is served as normal. The Result I feel this graph speaks for itself: This graph shows the number of requests that were blocked per minute. These bursts are the malicious scanners that I'm working to block, and all of these were successful defences against them. This will be a never-ending fight, but that's part of the fun, innit?
According to the Wayback Machine, I launched my website over a decade ago, in 2013. Just that thought alone makes me feel old, but going back through the old snapshots of my websites made me feel a profound feeling of longing for a time when having a website used to be a novel and enjoyable experience. The Start Although I no longer have the original invoice, I believe I purchased the ianspence.com domain in 2012 when I was studying web design at The Art Institute of Vancouver. Yes, you read that right, I went to art school. Not just any, mind you, but one that was so catastrophically corrupt and profit-focused that it imploded as I was studying there. But thats a story for another time. I obviously didn't have any real plan for what I wanted my website to be, or what I would use it for. I simply wanted a web property where I could play around with what I was learning in school and expand my knowledge of PHP. I hosted my earliest sites from a very used Dell Optiplex GX280 tower in my house on my residential internet. Although the early 2010's were a rough period of my life, having my very own website was something that I was proud of and deeply enjoyed. Somewhere along the way, all that enjoyment was lost. And you don't have to take my own word for it. Just compare my site from 2013 to the site from when I wrote this post in 2024. Graphic Design was Never My Passion 2013's website has an animated carousel of my photography, bright colours, and way too many accordions. Everything was at 100% because I didn't really care about so much about the final product more as I was just screwing around and having fun. 2024's website is, save for the single sample of my photography work, a resume. Boring, professional, sterile of anything resembling personality. Even though the style of my site became more and more muted over the years, I continued to work and build on my site, but what I worked on changed as what I was interested in grew. In the very early 2010s I thought I wanted to go into web design, but after having suffered 4 months of the most dysfunctional Art school there is, I realized that maybe it was web development that interested me. That, eventually, grew into the networking and infrastructure training I went through, and the career I've built for myself. Changing Interests As my interests changed, my focus on the site became less about the design and appearance and more towards what was serving the site itself. My site literally moved out from the basement suite I was living in to a VPS, running custom builds of PHP and Apache HTTPD. At one point, I even played around with load-balancing between my VPS and my home server, which had graduated into something much better than that Dell. Or, at least until my internet provider blocked inbound port 80. This is right around when things stopped being fun anymore. When Things Stopped Being Fun Upon reflection, there were two big mistakes I made that stole all of the enjoyment out of tinkering with my websites: Cloudflare and Scaling. Cloudflare A dear friend of mine (that one knows I'm referring to it) introduced me to Cloudflare sometime in 2014, which I believe was the poison pill that started my journey into taking the fun out of things for me. You see, Cloudflare is designed for big businesses or people with very high traffic websites. Neither of which apply to me. I was just some dweeb who liked PHP. Cloudflare works by sitting between your website's hosting server and your visitors. All traffic from your visitors flows through Cloudflare, where they can do their magic. When configured properly, the original web host is effectively hidden from the web, as people have to go through Cloudflare first. This was the crux of my issues, at least with the focus of this blog post, with Cloudflare. For example, before Lets Encrypt existed, Cloudflare did not allow for TLS at all on their free plans. This was right at the time when I was just starting to learn about TLS, but because I had convinced myself that I had to use Cloudflare, I could never actually use it on my website. This specific problem of TLS never went away, either, as even though Cloudflare now offers free TLS - they have total control over everything and they have the final say in what is and isn't allowed. The problems weren't just TLS, of course, because if you wanted to handle non-HTTP traffic then you couldn't use Cloudflare's proxy. Once again, I encountered the same misguided fear that exposing my web server to the internet was a recipe for disaster. Scaling (or lack thereof) The other, larger, mistake I made was an obsession with scaling and security. Thanks to my education I had learned a lot about enterprise networking and systems administration, and I deigned to apply those skills to my own site. Aggressive caching, using global CDNs, monitoring, automated deployments, telemetry, obsession over response times, and probably more I'm not remembering. All for a mostly static website for a dork that had maybe 5 page views a month. This is a mistake that I see people make all the time, not even just exclusive for websites - people think that they need to be concerned about scaling problems without first asking if those problems even or will ever apply to them. If I was able to host my website from an abused desktop tower with blown caps on cable internet just fine, then why in the world would I need to be obsessing over telemetry and response times. Sadly things only got even worse when I got into cybersecurity, because now on top of all the concerns with performance and scale, I was trying to protect myself from exceedingly unlikely threats. Up until embarrassingly recently I was using complicated file integrity checks using hardware-backed cryptographic keys to ensure that only I could make changes to the content of my site. For some reason, I had built this threat model in my head where I needed to protect against malicious changes on an already well-protected server. All of this created an environment where making any change at all was a cumbersome and time-consuming task. Is it any wonder why I ended up making my site look like a resume when doing any changes to it was so much work? The Lesson I Learned All of this retrospective started because of the death of Cohost, as many of the folks there (myself included) decided to move to posting on their own blogs. This gave me a great chance to see what some of my friends were doing with their sites, and unlocking fond memories of back in 2012 and 2013 when I too was just having fun with my silly little website. All of this led me down to the realization of the true root cause of my misery: In an attempt to try and mimic what enterprises do with their websites in terms of stability and security, I made it difficult to make any changes to my site. Realizing this, I began to unravel the design and decisions I had made, and come up with a much better, simpler, and most of all enjoyable design. How my Website Used to Work The last design of my website that existed before I came to the conclusions I discussed above was as follows: My website was a packaged Docker container image which ran nginx. All of the configuration and static files were included in the image, so theoretically it could run anywhere. That image was being run on Azure Container Instances, a managed container platform, with the image hosted on Azure Container Registries. Cloudflare sat in-front of my website and proxied all connections to it. They took care of the domain registration, DNS, and TLS. Whenever I wanted to make changes to my site, I would create a container image, sign it using a private key on my YubiKey, and push that to Github. A Github action would then deploy that image to Azure Container Registries and trigger the container to restart, pulling the new image. How my Website Works Now Now that you've seen the most ridiculous design for some dork's personal website, let me show you how it works now: Yes, it's really that simple. My websites are now powered by a virtual machine with a public IP address. When users visit my website, they talk directly to that virtual machine. When I want to make changes to my website, I just push them directly to that machine. TLS certificates are provided by Lets Encrypt. I still use Cloudflare as my domain registrar and DNS provider, but - crucially - nothing is proxied through Cloudflare. When you loaded this blog post, your browser talked directly to my server. It's almost virtually identical to the way I was doing things in 2013, and it's been almost invigorating to shed this excess weight and unnecessary complications. But there's actually a whole lot going on behind the scenes that I'm omitting from this graph. Static Websites Are Boring A huge problem with my previous design is that I had locked myself into only having a pure static website, and static websites are boring! Websites are more fun when they can, you know, do things, and doing things with static websites is usually dependent on Javascript and doing them in the users browser. That's lame. Let's take a really simple example: Sometimes I want to show a banner on the top of my website. In a static-only website, unless you hard-code that banner into the HTML, doing this from a static site would require that you use JavaScript to check some backend if a banner should be displayed, and if so - render it. But on a dynamic page? Where server-side rendering is used? This is trivially easy. A key change with my new website is switching away from using containers, which are ephemeral and immutable, over to using a virtual machine, which isn't, and to go back to server-side rendering. I realized that a lot of the fun I was having back then was because PHP is server-side, and you can do a lot of wacky things when you have total control over the server! I'm really burying the lede here, but my new site is powered not by nginx, or httpd, but instead my own web server. Entirely custom and designed for tinkering and hacking. Combined with no longer having to deal with Cloudflare, has given me total control to do whatever the hell I want. Maybe I'll do another post some day talking about this - but I've written enough about websites for one day. Wrapping Up That feeling of wanting to do whatever the hell I want really does sum up the sentiment I've come to over the past few weeks. This is my website. I should be able to do whatever I want with it, free from my self-imposed judgement or concern over non-issues. Kill the cop in your head that tells you to share the concerns that Google has. You're not Google. Websites should be fun. Enterprise tools like Cloudflare and managed containers aren't.
More in technology
Sharing a few data points because some people did not expect these drives to last that long.
I'm 100% for banning social media for children. I would even extend it to banning it all together in its current form. But I am also against the methods for enforcement. The problem with banning social media for one group and not another is that you have to differentiate the different groups somehow. And right now, in an age where information and personal data means everything, there are several entities that are salivating to get this data. I've written before about all the problems that come from uploading sensitive data online. It doesn't even matter if they delete the data afterward, as long as the data goes through the wire in the first place, there is a chance for a leak. What appears to be the same simple request "please verify your identity", becomes fundamentally different when mediated by technology. The question isn't whether these digital systems will be compromised, but when. And unlike that movie theater clerk who can't perfectly recall my birthdate minutes after seeing it, computers have perfect memory. They store, copy, backup, and transmit our most sensitive information through networks we don't control, to companies we've never heard of, under policies we'll never read. When kids are not allowed to use social media, it's the adult who pays the price. It's the adult who has to prove that they are not children. They have to risk uploading their government ID to a third party, or biometric data that may be saved and backed up forever. I reject this method. I also reject the method where I have to provide my age to the operating system. No system should compel me to provide personal data in order to use it. Especially if this feature is intended to provide the information to a third party. And what happens if I don't provide this information to my freebsd machine? Or worse, what if I provide false information? Am I going to get punished? How does that protect children in any way? Breaking news: Grown up arrested when OS reported forged identity in his laptop. One question I asked myself was what happens when a minor circumvents the ban. What are the consequences? At least in Australia, a country that has implemented a social media ban for those under 16, there are no consequences. The law places legal responsibility on platforms, not children or parents, and it currently applies to Facebook, Instagram, Kick, Reddit, Snapchat, Threads, TikTok, Twitch, X and YouTube. This almost sounds reasonable. Why should you care, if it becomes facebook responsibility when you don't even own a facebook account? But there is always a caveat. When facebook becomes a tool your school uses to communicate with parents. When Google classroom is the service students use to do their homework. When you are required to login on a system to receive government service. You may not care about a particular social media, but they raise the bar for every other entrant in the game. Every website frequented by a large number of people inherently become social media, and demand data you didn't need to provide before. Technology is not a magical thing that can just solve every problem. It is especially weak when it comes to social problems. However, society can use technology to solve their problems. The responsibility of children falls onto their parents. And there are great tools for parents that their kids access on their devices.
Introduction Hamming Distance Intuitive Estimation of Error Correction Bits Digging Deeper Comparing against BCH Codes What about Reed-Solomon Codes? BCH vs Reed-Solomon Codes References Footnotes Introduction I’ve been interested in error correcting codes (ECC) for a while now, and though I haven’t gotten very far, I keep on reading and, occasionally, writing about it. Except for really simple schemes, ECC theory gets complicated quickly. The two codes that I’ve looked at most are Reed-Solomon (RS) and binary Bose–Chaudhuri–Hocquenghem (BCH) codes. They are closely related: both use polynomial math that is normally performed over a Galois Field, though, as shown in my earlier Reed-Solomon blog post, that’s not strictly required. The encoding algorithms are relatively straightforward but the decoding algorithms are significantly more complicated. RS and binary BCH codes differ in the size of their symbols, their alphabet: a symbol of the former consists of multiple bits, often 8 bits, while a symbol of the latter is a bit. Errors are corrected with a granularity of a symbol. One key ECC question is “How many additional bits do I need to correct a given number of errors?” In this blog post, I check if an intuitive approach that I’ve been using matches reality. I’m trying hard to stay away from theory here, which makes the subject hopefully more accessible, but has the flip side that it’s far from being rigorous and exact. Hamming Distance When you look at the theory of error detecting and correcting codes, the discussion immediately turns to Hamming distance: the number of bit flips required to convert one vector of bits to another. The goal of an error correcting code is to transform the input vector, the message, to an output vector, the codeword, so that the minimum Hamming distance between valid codewords is guaranteed to be at least a certain number. The higher the Hamming distance, the higher number of bits that can be flipped in a codeword without destroying the ability to recover the original message. If \(t\)1 is the number of errors that we want to correct, error coding theory gives us the following formula: In words: to correct a codeword with a maximum number of \(t\) bit errors, the Hamming distance between any 2 codewords must be larger than two times \(t\). The theory also tells us the minimum Hamming distance to detect an error without correcting it: In the rest of this blog post, though, we’ll only care about correction, not detection. Here are 2 quick examples where the message is one bit. Adding 1 bit to the message For a one bit message, there are only 2 possible messages, 0 and 1. Let’s use a code that repeats this bit once: 0 becomes 00 and 1 becomes 11. The Hamming distance between these 2 valid codewords is 2 because you need to flip 2 bits to convert 00 into 11. The equation above tells us that a distance of 2 is not sufficient to fix a one-bit error. Indeed, when we receive 01 as a codeword, it’s impossible to tell if the transmitted codeword was 00 or 11: both have a Hamming distance of 1 to the received word of 01. Adding 2 bits to the message When we expand the 1 bit message to 3 bits and repeat the message bit twice, things are different: 0 transforms into 000 and 1 into 111. The Hamming distance between all possible codewords is 3. The theory says that we should be able to correct a codeword for which only 1 bit was flipped. And indeed, if we receive 001 and there is only 1 bit corrupted, we know for sure that the original message was 0. Intuitive Estimation of Error Correction Bits The concept of a minimum required Hamming distance to correct a number of errors is not particularly complicated, but it’s a different matter to use it to determine the number of additional ECC bits for, say, a BCH code. In fact, I still don’t know how to do it. As an alternative, I found myself using a simple model that works like this: Participant A transmits 1000 bits to participant B over a noisy channel B transmits the same 1000 bits back to A over a perfect channel A transmits a 10 bit error index to signal to B which bit it received wrong Or more general: If a message of \(k\) bits has 1 incorrect bit, you need a number of \(r = \lceil \log_2(k) \rceil\) 2 bits to point out the incorrect bit. Let’s call that number the error index3. To correct \(t\) bits, you need up to \(r = t \cdot \lceil \log_2(k) \rceil\) additional bits. That’s it! The model is simple, it’s not exactly practical, and the number of parity bits has to be only a first order estimation, but I felt like it had to be pretty close to reality. But is it? Digging Deeper Let’s consider some of the aspects of this model: There is no way to signal that there was no corruption That can be solved by allocating one error index value extra to signal a perfect transmission and calculate the number of bits as \(r = \lceil \log_2(k+1) \rceil\) instead of \(r = \lceil \log_2(k) \rceil\). E.g. for a 1000-bit message, an error index from 0 to 999 indicates an error location, and 1000 signals perfect transmission. For messages that are not a power of 2, the number of error indicator bits remains the same. The transmitted error indicator bits can be corrupt as well One way to solve this is by going through the same procedure to verify the error index. For a first iteration, it would cost an additional \(\lceil \log_2(\lceil \log_2(k+1) \rceil ) \rceil\) bits, 4 bits for a 10 bit error index. Of course, those 4 bits might be corrupt too! A better approach is to work backwards: start out with the length of the codeword and the number of errors you want to correct and determine the number of input message bits from there. The number of error indicator bits is then calculated over the length of the codeword and you get \(r = \lceil \log_2(n+1) \rceil\). The length of the message is \(k = n - r\). This breaks the simple model where you transmit message and error indicator separately, but that was not a realistic scenario anyway. It doesn’t consider that the same error index can appear only once If the code can correct 2 errors, you’d need \(\lceil \log_2(k+1) \rceil\) bits for the first error index to indicate zero errors or the location of the first error. For the second error, you need one less value because one location has already been corrected, so the number of bits needed is \(\lceil \log_2(k) \rceil\). This suggests that the number of error indicator bits might be an upper bound. It only applies to error correction the way BCH does it Any random bit can be corrected, but it doesn’t work for RS coding because there a whole symbol can be incorrect. That’s fixable though, I get to that later. You don’t need to signal the correct value Since the symbol is 1 bit in size, the location of the error is sufficient. To fix it, the receiver just needs to invert the bit. This also doesn’t work for RS. Comparing against BCH Codes Forget about using the Wikipedia article to compare the intuition against theory: you won’t find anything explicit. I found this presentation from the National Taipei University useful. The key formulas are on the first slide: Block length: \(n = 2^m -1\) Number of parity-check bits: \(n - k \le m t\) Minimum distance: \(d_{min} \ge 2t + 1\)4 In the equations above, \(k\) is still the length of the original message, \(t\) is the number of bits that can be corrected, \(n\) is the length of the codeword. If we rearrange the first equation, we get: Substitute \(m\) in the second equation: \(r\) is the number of error correction bits, so \(r = n - k\) and: Compare this with the intuitive formula that I started with: The only differences are: I used \(k\) instead of \(n+1\). That accounts for the fact that parity bits themselves need error correction too and that a perfect transmission must be signaled. the BCH equation assumes that the total codeword length is always one less than a power of two. instead of an approximation, it defines an upper bound. Theoretically, all BCH and RS codewords have a length \(n = 2^m -1\) symbols. In practice, the transmitted codewords are often shortened by setting a bunch of the symbols in the input message to zero and not transmitting them.5 When using shortened codes, you still have to use the \(m\) value of the parent code for which \(n = 2^m -1\). The intuitive formula works for long codewords and a limited number of errors to correct: \(n \approx k\). What about Reed-Solomon Codes? RS codes correct symbols not bits but you can still use the intuitive formula to identify the corrupted symbol: in a message with, say, 223 bytes you’d need 8 bits to pinpoint an incorrect byte. However, you also need an additional byte to fix the corruption and that is the case for every error byte that needs to be corrected. To repair \(t\) symbols that are \(s\) bits in size in a message of \(k\) symbols, you’d need this number of bits for the error location: And a bunch of bits with the correct values: Note that \(r_{location}\) points to a symbol, not a bit. Just like BCH codes, \(n = 2^s - 1\) for RS codes. Change \(r_{location}\) to use \(n\) instead of \(k\) and you get: The final result is: It turns out, that is exactly what you’ll find in all texts about RS coding: the number of parity symbols must be at least 2 times the number of corrupt symbols you want to correct. BCH vs Reed-Solomon Codes I’m getting a bit off-topic here but with the intuitive formula validated, it’s interesting to see when RS codes are better than BCH and vice versa. Let’s create an example where codewords are 255 bytes or 2040 bits and where we reserve 32 bytes or 256 bits for error correction. For RS, this allows us to correct 16 corrupt bytes that can contain up to 128 corrupt bits, as long as they’re contained within those 16 bytes. For BCH, we need 11 bits per error or \(11 \cdot 23 = 253\) parity bits to correct 23 corrupt bits which fits within the 256 bits that were allocated for RS. RS is the winner if errors happen in bursts: it doesn’t matter if a burst is 1 or 8 bits long, it counts as a single corruption when they fall within the same byte. BCH wins when errors are spread randomly: instead of fixing 16 bit flips, it can fix 23. In practice, communication protocols often used 2 codes in the following scheme: Transmitter: RS Encoder -> Interleaving -> Convolutional Encoder Receiver: Viterbi Decoder -> De-interleaving -> RS Decoder In this setup, RS is called an outer code and convolutional coding the inner code, because they wrap around the transmission in that order. In many transmission mediums, bit error are uncorrelated and burst errors unlikely. One would think that RS coding is the wrong choice then. However, the Viterbi decoder can introduce bursts of errors when it makes a mistake. The RS decoder is used to clean those up. The burst error length can be so long that a single RS codeword can’t deal with them. That’s why there’s an interleaver to increase the length of burst errors that can be corrected, at the expense of latency and memory storage, but that’s a topic for a different blog post. Modern communication protocols, such as DVB-S2 that’s used by satellite TV, use a low density parity check (LDPC) code instead of a convoluational code as inner code. A LDPC decoder has a lower tendency to introduce long error bursts in case of a mistake and instead introduces single bit errors that are spread randomly within a large codeword. This is where BCH codes shine which is why it’s used instead of RS. All words in this blog posts were written by a human. References Reed-Solomon Error Correcting Codes from the Bottom Up National Taipei University - BCH code CMU Introduction to Coding Theory - Notes 4: Elementary bounds on codes NASA - Bandwidth efficient CCSDS coding standard proposals Discusses outer coding/interleaving/inner coding for space communication. There’s an abundance of information in there, which is what you’d expect from a 70 page document! Footnotes In literature, you’ll often see \(k\) being used for this formula, but further down, \(k\) is used as the length of the original message, which is also a literature convention. So I decided to be consistent within this blog post. ↩ \(\lceil x \rceil\) means: round up to nearest integer. ↩ I came up with “error index” myself, but it turns out to be a term that is commonly used in error coding theory and its meaning is exactly what I had in mind! ↩ This is the minimum Hamming distance equation for error correction. ↩ This assumes that a systematic coding scheme is used, where the original message appears unmodified in the codeword. This is the case for most BCH and RS coding schemes that are used in the real world. ↩
Plus Thomas Ableman on what we can learn about AI from the Victorian railway rollout
Water-resistant microneedles made from food-grade silk could deliver vaccines to help prevent the spread of disease in aquaculture.