Thursday, May 28, 2026

Promoting and pushing developers to use AI

 At Octopus, leadership is calling for people to develop fluency with AI, and to start adopting it if they haven't been already.

I've had a few chats with people about it, and feelings about it. Here is my take, and some bits from those discussions in case they are helpful to anyone else.

Once you get to Senior Software Engineer, the primary reason you get paid switches away from the ability to write code (at this level, everyone can do this well, it's just table stakes) and towards Decision Making and Good Judgement. Thinking about tradeoffs, pros and cons, effects of designs on humans and other parts of the system, etc.
  • This has always been the case. If you are at a lower level, once you've proven your technical coding ability, that's what you're shooting for.

AI brings new tools to the table. Just like you probably wouldn't write a string-scanner by hand when regex is available, if AI can write a piece of code in 10 minutes that might take you 3 hours, you probably should use AI.Like any other tool, we can't make good decisions about when to use AI and when not to, if we don't understand it or can't effectively operate it. So we should learn.

Making good decisions also means understanding when not to use AI, and how to validate the results.
  • AI helps you move quickly but it carries zero authority. If AI says the right answer is X, that means nothing unless you back it up, with external research, testing, or other source of truth.
  • This applies to any code it generates. You need to back it up.
    • If it generates code, you should review and run that code.
    • If it generates tests, you should review and run those tests.
    • If it adds a comment referencing some quirk in a framework, you need to understand that quirk and whether it actually applies.
    • and so on.

I find, if I have a clear and detailed idea of exactly what I want the result to be, I can describe this to AI, and these days it usually does a good job. But if I don't have a clear idea of the solution (design, code structure, etc), then I cannot make good decisions or judge anything that an AI might produce, so it is upon me to go and figure that out. Usually that involves exploratory "manual" coding.For me, this is where the interesting parts of software development have always been, and I'm not bothered about losing any coding-time towards AI. I'm always the one in charge of deciding whether to use AI for any given task because I'm being paid to use my good judgement about these things.

To recap: Nobody should use AI just because a senior person said so. But you're responsible for making good decisions, and sometimes the best might be to use AI. Get yourself to the point where you can make that decision confidently đŸ˜€

(With honesty: I'm not at that point yet myself. There are some places where I'll write code by hand for ages and then think, hmmm, did I just waste a bunch of time? and some other times where I've tried to use AI and it's taken longer than it would have by hand. Maybe nobody ever gets there completely, because the field keeps moving. But that's OK.)

Saturday, January 24, 2026

Costs and Experience using a Wise card on our recent UK holiday

My family recently took a vacation, where we spent roughly 4 weeks in the UK (England, Wales, Scotland) and a week in Paris. I live in New Zealand, and my "native" currency is the New Zealand Dollar.

Before we left, I'd heard about Wise and their currency card via various YouTube creators promoting it, and other things like it. It's easy to see from the various websites that Wise charges a 0.3% currency conversion fee, whereas overseas purchases made on my ASB Bank Visa Card incur a 2.1% offshore service margin. Buying a Wise card costs $14 NZD, and I remember doing some very rough maths at the time, which told me that if we spent more than (I think) $600 NZD on the Wise card, we'd come out ahead.

Aside: Why Wise and not an alternative like Travelex or Cash Passport? It was quickly apparent that Wise had the best rates, and it was well reviewed in blog posts and articles that compared them. Cash Passport in particular, seems ridiculous, with it's 5.95% currency conversion fee. That's nearly 3x what my bank Visa charges, why would anyone buy one of these things?

So, I signed up for Wise, bought the card, and off we went.

But how did it turn out in reality? Now that we're back at home, I've sampled some data from my bank and Wise statements to find out.

Overall

For every $1000 NZD I spent with Wise, I lost $2.99 NZD to currency conversion fees
For every $1000 NZD I spent on my ASB Visa, I lost $13.7 NZD to a combination of fees and worse exchange rate.

If you're spending more than $1300 NZD, then (compared to an ASB Visa like mine) the Wise card pays for itself. The more you spend, the more you benefit, but in the grand scheme of things it wasn't that big of a deal.

I'm glad I got (and used) the wise card. It did save us some money, and their mobile app and web systems are really nice, but there were also some annoyances. These prevented us from using it as much as we might have liked and were a bit disruptive sometimes when making purchases.

Experience

The main way of managing Wise is via their Mobile App. I used the iPhone app, which was fantastic. It's really well designed, highly polished and easy to use. It makes the ASB (and probably most other banks) mobile applications look like outdated dinosaurs.

To load money onto your card, you have multiple options, but the one I used was a direct bank transfer, as there are no fees or overhead in doing this. The mobile app makes this super easy - you select "Add Money", then it gives you an NZ bank account number to make a transfer into. This bank account number is unique to you, so you don't have to sweat about whether you've put the right magic numbers into the particulars/reference/etc. You just transfer the money to that account, and a bit later it shows up in Wise, with a nice push notification to your phone to inform you.

Major Problem: Bank Transfer Delays

This was the main thing that prevented me from using Wise for all our overseas transactions. You have to preload money into the card, and if it runs out you need to transfer some more.

I found that on weekdays during business hours, these transfers would complete in 1-3 hours. That's fine if you make a transfer in the evening for the next day, or even in the morning before you start traveling out to wherever you're going that day, but there were a number of times where we needed something "on the spot" and the transfer time was simply too slow.

On the weekends, the international banking system seems to go to sleep. I made a transfer one Friday evening, and it didn't show up until some time on Monday. This left me unable to use the Wise card for the entire weekend. I don't think this is Wise's fault, just how the banking system works, but if I were a banker, I would be embarrased. It's 2026! There's no reason whatsoever why important functionality like electronic money transfers needs to stop just because it's Saturday.

Update: Various readers from Australia, the US and UK have let me know that the bank transfer delay problem hasn't happend to them. It seems like it may be a unique characteristic of the antiquated NZ banking system, thanks to the shared monopoly that the four big australian-owned banks have over it.
If you aren't from New Zealand, you may have a much better experience!

Minor Problem: Randomly getting prompted for PIN input

I'm accustomed to being prompted for a PIN when making contactless transactions over a certain dollar value at home, and overseas was similar using the ASB visa. Many places will have a sign or something that says PIN required for contactless over $X, and when I was prompted in this manner, it was fine.

The Wise card however, seems to also randomly prompt for a PIN on transactions at any value level every so often. I'm not sure why this happens, perhaps it's a "security" feature on their part, but it's very annoying because it's completely unpredictable. Additionally, when it happens it seems to manifest as the transaction declining, and then re-prompting, rather than just a simple "ask for PIN". This happened about 10 times for me, and whenever it did it seemed to be quite confusing to the cashier. Most of the time they would say something like "That transaction failed sorry sir, I think you might need to try again with a PIN?", but one some occasions they were flummoxed, and I ended up falling back to the ASB visa. If you work for Wise, please fix this.

Workaround: Use Apple Pay
I added my Wise Card to Apple Pay on my phone for convenience, but through trial and error I discovered that this random fail / PIN prompt only happend when using the physical Wise card, and never when. using it via Apple Pay. Thereafter I basically stopped using the physical card entirely, and didn't have the problem any more.

Trivial Problem: I couldn't use my Wise card for Apple Pay Express Transit

In London, Rail, Subway and Buses all allow you to tap on and off using a contactless payment card (typically a credit or debit card). You can use your phone for this too, provided you have Apple Pay active with a linked Credit card. Apple calls this Express Transit. I assume Google/Android have something similar.

My phone had both my ASB Visa and Wise card activated for Apple Pay, and I was able to use both interchangeably when making purchases, but when I tried to set the Wise card as the active one for express transit, it simply failed saying it could not activate. I don't know if this is a limitation of the Wise card, or just a transient bug I hit at the time. It wasn't a big deal, I was able to use my phone for buses and trains, and it linked back to the ASB Visa, but it would have been nice to send these through Wise as well.

(To be clear, the Physical Wise card worked fine as a contactless payment for London Transport)

Fees

Rough summary:
For every NZD $1000 I spent with Wise, I lost $3 NZD to fees
For every NZD $1000 I spent on my ASB Visa, I lost $11 NZD to fees

Wise Card

The way that Wise models different currencies is a bit like having multiple accounts behind the one card. Your card can hold some amount of each currency (up to 40). At one point I had some NZD, GBP and EUR of varying amounts all in it at the same time. You only pay a fee when you convert between currencies.

As promised, there were no fees transferring money to my Wise card from my NZ bank account using a direct bank transfer, and when I converted NZD to GBP, I was charged a 2.99% fee for this. The fee didn't vary, whether you converted $1000 or $10.

ASB Visa (baseline)

My NZ bank and Visa card are in NZD, and there's no additional fee when purchasing things in NZD. When purchasing things in foreign currencies however, you end up with two transactions in your statement, roughly like this:

Description Amount
OFFSHORE SERVICE MARGINS $6.89
M&S BRISTOL CABOT CIRCUS LONDON
140.40 POUND STERLING
at a Conversion Rate* of 0.4280 (NZ$328.06)
$328.06

(You can do the maths to verify the advertised 2.1% service margin)

However, I'm also enrolled in ASB's True Rewards loyalty program, which for my card gives back 1% of the spent amount in rewards dollars. Rewards dollars are 1:1 with NZD, and we can spend them a many places we regularly shop in, so it's effectively 1% cashback.

Before we left, I did some quick searching and I was unable to find anything definitive as to whether I'd get rewards dollars on foreign currency purchases. I assumed that I wouldn't earn them, however upon inspecting bank statements after the trip, I've verified that I did indeed earn rewards dollars on overseas purchases. 

For the time period that we were away, I added up the amount spent on my card (which was almost entirely foreign currency purchases), and compared against the rewards dollars. I found that (once removing the offshore service margin entries) the rewards dollars earnt exactly matched the amount spent. This brings the net overseas currency conversion fee down to around 1.1%.

Exchange Rates

Rough Summary:
Wise gave better exchange rates, by roughly 0.27%

Wise says they use the mid-market conversion rates. I wasn't able to find anything definitive about which rate ASB uses.

The GBP <-> NZD exchange rate was roughly around 0.43 (1 NZD buys you 0.43 GBP) for most of the trip, however in the detail it fluctuated quite a bit. The first conversion I did using Wise on 17 December 2025 used a rate of 0.4332, while the last one on 16 January 2026 used a rate of 0.4284. Both the Wise and ASB

Because I was using both the ASB Visa and the Wise card at the same time on the trip, I was able to compare transactions on the same day, to discover if there was actually a difference. One example:

On the 8th of January, I used Wise to convert some NZD to GBP. This used an exchange rate of 0.4293

On the same day, I made three transactions on my ASB Visa card. These used exchange rates of 0.4266, 0.4267 and 0.4266.

This rate-difference applied to other transactions on other days, and overall the Wise rate was better by roughly 0.0027, or 2.7%

Sidenote: Auto vs Manual currency conversion with Wise

At first, I loaded money into the Wise card, and then immediately converted all of it into GBP.

However, wise has a feature wherein if you make a purchase in a currency, but your card doesn't have any GBP in it, then on the fly they will automatically convert from a currency you do have loaded. I ended up using this feature by accident because I loaded some NZD but forgot to convert it.

These automatic conversions always worked, and looking through transaction records, they didn't cost me any more than had I done a manual conversion. This wise help article says personal customers are limited to 15 currency conversions in 24 hours, so you might not want to rely on auto-conversion if you're using it for every transaction.

If we did another trip like this, I think I would do something like
- Load larger amounts of money into Wise per batch but leave it in your home currency
- Each day, do a currency conversion to your target currency for roughly what you expect/budget to spend. If you go over that number, the auto-conversion will kick in and you'll be fine
- At the end of the trip, you may still have some money left, but most of it should still be in the home currency, and you could transfer it back out of Wise with no penalty

Conclusion

Despite the cost savings being relatively minor, and the annoyance of the bank transfer delay times, I was still very glad that I had the Wise card. The increased peace of mind of having multiple cards while travelling - should one get cancelled, frozen or stolen - was definitely worth the cost, even if I'd saved $0. I think the next time I go on a work trip to Australia and have to expense things back to the company, I'll put them all on the Wise card to make the foreign-currency accounting much simpler, and keep them separate from my normal Visa. 

Of course, this is just my experience and comparison with my own existing Bank Visa. Different banks have wildly different credit cards, products, and conversion rates, so while Wise was marginally better for me, it might be significantly better for you. Good luck!

Saturday, July 05, 2025

Patching the .NET 8 runtime to fix debugging performance on macOS and Linux

The .NET runtime has a bug which makes debugging extremely slow on macOS and linux when working with code that does a lot of async/await. 

One example: Running one of the Integration Tests for the Octopus Deploy server took roughly 30 seconds when run on Windows with a Debugger attached. When running on either Windows or Mac without a debugger it took roughly 25 seconds, but on Mac with a Debugger it took over 3 minutes.
With a patched version of the .NET runtime, the MacOS debugger time drops down to match the non-debugger time.

The issue was tracked here in Debugger.NotifyOfCrossThreadDependency can be slow when a debugger is attached and was fixed in .NET 9. However .NET 9 is a short-term-support release and a lot of codebases can't upgrade to it. 

To fix the issue on .NET 8 or lower, you need to build a patched version of System.Private.CoreLib.dll and put it into your local .NET runtime folder. This needs to be done for every new runtime version (e.g. 8.0.16 and 8.0.17)

At Octopus we were hit by the issue in the days of .NET 6, so we needed to patch the runtime, or put up with debugging performance so bad that it nearly took macOS off the table as a developer option. To solve it at scale, we wrote a GitHub actions script to build the patched version of the runtime, so developers could simply download a patched dll and install it on their macs. Here's how it works.

Note: This is not novel or OctopusDeploy-specific information. We got this information from various comments and discussions on GitHub issues where it is public. I would link to them except it was a few years ago and I can't recall or easily find the exact ones.

Anyway, to resolve the issue you need to turn the NotifyOfCrossThreadDependencySlow function in src/coreclr/System.Private.CoreLib/src/System/Diagnostics/Debugger.cs into a no-op. Our GitHub action does it by applying this patch file:

diff --git a/src/coreclr/System.Private.CoreLib/src/System/Diagnostics/Debugger.cs b/src/coreclr/System.Private.CoreLib/src/System/Diagnostics/Debugger.cs
index 9a51c47cb02..2bbd33efb74 100644
--- a/src/coreclr/System.Private.CoreLib/src/System/Diagnostics/Debugger.cs
+++ b/src/coreclr/System.Private.CoreLib/src/System/Diagnostics/Debugger.cs
@@ -32,8 +32,10 @@ private sealed class CrossThreadDependencyNotification : ICustomDebuggerNotifica
 
         // Do not inline the slow path
         [MethodImpl(MethodImplOptions.NoInlining)]
-        private static void NotifyOfCrossThreadDependencySlow() =>
-            CustomNotification(new CrossThreadDependencyNotification());
+        private static void NotifyOfCrossThreadDependencySlow()
+        {
+            // Removed as per the .NET debugger on macOS hack
+        }
 
         // Sends a notification to the debugger to indicate that execution is about to enter a path
         // involving a cross thread dependency. A debugger that has opted into this type of notification

Aside: You might wonder if there are any harmful side-effects of no-opping this function. Presumably it does something useful? At Octopus Deploy, we've had roughly 50 macOS-using developers doing full time .NET development with this patch since 2023 across many versions of .NET 6 and .NET 8, and there was not one single report of it causing any problems. So from that perspective, it seems 100% safe.

You need to compile the .NET Runtime codebase in release mode with this patch applied, which goes like this:

1. You need to do this on an Apple Silicon mac, and you need the Xcode command line developer tools installed if you don't already have them. You can do this by running xcode-select --install in the terminal. MacOS will prompt and ask if you want to install the command line tools.

2. Once you have the command line tools you also need cmake, icu4c and pkg-config. You can get these via homebrew with brew install cmake icu4c pkg-config. Note: If you're allergic to homebrew like I am, there's a chance these may not be needed on later versions of macOS. You could try skipping this step and proceeding with building the patch without them and see if it works for you, then loop back and add them if not.

3. Find the runtime version you want to patch. For example 8.0.17. You can see which runtimes you have by running dotnet --list-runtimes. If you have multiple installed, you probably just need to patch the latest 8.0.x version.

4. Second, check out the dotnet/runtime Git repository and checkout the tag matching that version. Microsoft put 'v' in front of their tags, so you'd check out refs/tags/v8.0.17

5. Third, apply the patch. If you're just doing it as a one-off the easiest way is to open the aforemorentioned Debugger.cs in a text editor and simply edit it to no-op the NotifyOfCrossThreadDependencySlow function body. 

6. Then you can build the runtime by running build.sh clr -arch arm64 -c release inside the checked-out dotnet runtime repository.

7. The build is going to drop a file into runtime/artifacts/bin/coreclr/OSX.arm64.Release/System.Private.CoreLib.dll. You want to copy this into your 'real' dotnet runtime installation by doing something like sudo cp ./runtime/artifacts/bin/coreclr/OSX.arm64.Release/System.Private.CoreLib.dll /usr/local/share/dotnet/shared/Microsoft.NETCore.App/8.0.17/

That's it! Restart your instances of JetBrains Rider / VSCode or whatever other tools you're using to work with .NET applications and you should observe vastly increased Debugging performance.

For completeness, here's our complete GitHub actions yaml workflow file which automates this, if you'd like to do that too. 

name: CI

on:
  workflow_dispatch:
    inputs:
      version:
        description: 'Git tag in dotnet/runtime repository to check out (e.g. v6.0.22)'
        required: true
        type: string

jobs:
  build:
    # Run specifically on Apple Silicon runner: https://github.com/actions/runner-images/issues/8439
    runs-on: macos-latest-xlarge

    steps:
      - name: Install dependencies
        run: brew install cmake icu4c pkg-config
        
      - name: Checkout patch
        uses: actions/checkout@v4
        with:
          path: patch
        
      - name: Checkout .NET runtime
        uses: actions/checkout@v4
        with:
          repository: 'dotnet/runtime'
          ref: refs/tags/${{ inputs.version }}
          fetch-depth: 1
          path: runtime

      - name: Apply patch
        run: patch -N -i patch/Debugger.cs.patch runtime/src/coreclr/System.Private.CoreLib/src/System/Diagnostics/Debugger.cs

      - name: Build runtime
        run: runtime/build.sh clr -arch arm64 -c release
      
      - name: Compute hash
        run: shasum -a 256 runtime/artifacts/bin/coreclr/OSX.arm64.Release/System.Private.CoreLib.dll > dotnet_runtime_${{ inputs.version }}_SHA256SUM.txt

      # The create-release action created by GitHub is no longer maintained: https://github.com/actions/create-release.
      # At the time of writing, ncipollo/release-action is listed on the repo as one of the recommended alternatives
      - uses: ncipollo/release-action@440c8c1cb0ed28b9f43e4d1d670870f059653174 # v1.16.0
        with:
          tag: ${{ inputs.version }}
          artifacts: "runtime/artifacts/bin/coreclr/OSX.arm64.Release/System.Private.CoreLib.dll,dotnet_runtime_${{ inputs.version }}_SHA256SUM.txt"

Tuesday, October 13, 2020

End to End encryption in New Zealand

[Loud and obligatory disclaimer: These are my personal views and do not represent those of my employer, or anyone else]

Yesterday (12th October 2020), the New Zealand Government released this statement regarding end to end encryption:

https://www.beehive.govt.nz/release/international-statement-end-end-encryption-and-public-safety

This provoked a collective WTF amongst many IT professionals, but also some amount of understanding that we can't just shrug and "hide behind" strong encryption. While I can see that angle is absolutely valid, and I'd like there to be a nice answer that could provide security and privacy to people while also not impeding police investigations - there simply isn't. I come down on the side of the fence in favor of security and privacy, and support strong encryption.

After our country's amazing COVID response, my trust in our current Labour government was at an all-time high, and my interpretation of the beehive release was that perhaps they do understand encryption, but are being forced to toe the line behind the US, UK and Australia, as part of the Five-Eyes alliance. Ever the optimist I am! Anyway, as one does, I got involved in some twitter debate, and Andrew Little himself (the minister in charge of this sort of thing) replied with:

I don’t accept you get to dodge responsibility for permitting serious criminal offending on your platform(s). Technology isn’t an excuse to allow others to be abused & exploited. Not unreasonable to expect you to cooperate to investigate criminality at your on-line place.

https://twitter.com/AndrewLittleMP/status/1315566745671757825?s=20

So, clearly Mr Little buys into this. One would think that if the government were just capitulating to the five-eyes, he would have simply said nothing here.

So - where does that leave us? People far smarter and more articulate than I have already explained why banning strong encryption isn't a good idea. Here's a good one for starters: http://cyberlaw.stanford.edu/blog/2019/11/banning-strong-encryption-does-not-mean-catching-criminals-it-only-makes-you-less-safe. As it says, banning strong encryption doesn't mean catching criminals, it just makes you less safe from them. As a professional software developer with experience working on secure systems and having to worry about things like encryption, certificates and other kinds of secrets, I agree with that view. If you prevent everyone from using strong encryption, then the only people remaining who will actually be able to use it are the criminals! They're already breaking the law so they're hardly going to throw away their E2E code because of any new anti-encryption laws that might arise.

One might make an argument perhaps that if we ban E2E, then it at least raises the bar, so only the incredibly rich and powerful crime syndicates would be able to use and obtain strong encryption on the black market! This sounds appealing, but doesn't hold in reality. Anyone can download a free, open source implementation of the Signal Protocol right now from Github. The Signal Protocol is the gold standard in E2E encryption, and with open libraries like that, most reasonable developers could likely hook it all up and have a functioning system in a week or two. If the Signal libraries were to disappear, this kind of thing can always be rebuilt; A junior developer on my team built an ECIES messaging system (which is the same E2E encryption that Apple's iMessage uses) in about three months - and honestly we could have gone faster if we'd wanted to.
The bar is low, and that's only if the existing E2E apps such as WhatsApp, Facebook Messenger, the Signal app, etc. were all wiped from existence. Until that happens, criminals will simply install and use those, and because the software development bar is so low, if one app is wiped out, another will simply pop up to replace it. The cat is out of the proverbial bag.

What about collateral damage? I'm a software developer, and I work on cloud products. E2E is a really powerful tool for me; I can use it to let customers send data from their corporate servers down to client apps (on phones), and vice versa. The data can transit through the cloud, and E2E means that nobody can spy on it.  As a software vendor, I want to be able to provide strong security guarantees to my customers, and E2E lets me do that. If I'm banned from using E2E, and I must build a system which is theoretically capable of spying on customers on behalf of law enforcement, then that system is inherently worse. 

There's a binary switch here. With E2E, the customers don't have to trust me. They just have to trust that the software that they run on their phones and servers is acting in good faith. We have our software independently audited for that.
Without E2E, as much as we might say "we don't spy on you" or "we won't harvest your personal information" or any other such statements, the fact will always remain that we could if we wanted to, which erodes the entire foundation of trust on which such things are built. Secondarily, we might get hacked or have a data breach, and without E2E the consequences would be a lot more severe.

Where does that leave us?

In spite of the above beehive statement, I don't believe that the NZ Government wants to blindly institute a ban on all E2E (or otherwise "strong") encryption. An outright ban would erode trust internationally in our country, and could hurt export sales of NZ-owned but globally distributed software like Xero. Technology is NZ's third largest export, and I don't think the government wants to ruin that. Australia's anti-encryption laws are already hurting their tech sector, and it would be tragic if we didn't learn from their mistakes.
I hope that the government would allow it, but with consideration that there is an avenue for law enforcement to take in the event of a criminal investigation. In the example above, if I sell software to a business, and that software uses E2E to enable secure messaging between a business and it's employees, then law enforcement has a clear path to follow. They could compel the business to turn over all the audit records from their servers, and proceed.

What I'd like to see is some discussion between the tech community and the government around setting some expectations for where E2E might be appropriate. Clearly the "person-to-person" messaging scenario of WhatsApp/Facebook Messenger is not acceptable in the eyes of Government, but would a "business-to-business" scenario be? How about "business-to-person"? What if the business were overseas? Some guidelines would really help our IT industry navigate these waters. 

So, to Andrew Little - let's get a discussion going between NZ software developers and the government.
My inbox is always open. flick me an email - orion.edwards@gmail.com :-)

Thursday, October 24, 2019

How to pin a custom certificate trust chain and DNS name in C#

This code tested on dotnet core 3.0, however the API's are stable and this most likely works across all dotnet versions


// create a single long-lived HttpClient, that's what microsoft's documentation says to do
var handler = new HttpClientHandler {

    ServerCertificateCustomValidationCallback = (message, certificate, chain, policyErrors) => {
        // first check the DNS name in the certificate
        var expectedHostName = new Uri(host).DnsSafeHost;
        var certificateDnsName = certificate.GetNameInfo(X509NameType.DnsName, forIssuer: false);

        if (!certificateDnsName.Equals(expectedHostName, StringComparison.InvariantCultureIgnoreCase))
        {
            log.LogError($"Server Certificate rejected due to invalid subject name." + 
               " Certificate provides {certificateDnsName}, expecting {expectedHostName}");
            return false;
        }

        chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
        chain.ChainPolicy.ExtraStore.Add(caCertificate);

        // refer https://social.msdn.microsoft.com/Forums/vstudio/en-US/1966a6e8-b6f4-44d1-9102-ec3a26426789/how-can-i-verify-a-certificate-manually-without-installing-its-parents?forum=clr
        // we can't have a "trusted" root, because we're not affecting the machine-wide trust store, however
        // if a cert correctly chains back to our CA, then the only "error" will be a single untrustedRoot error, which means
        // everything else was good, just the root wasn't in the machine-trust store, so we're good

        // Note that if the chain terminates at a different root certificate that is trusted by windows, we
        // do NOT want to accept that cert, but it will log something like Validation errors: None which may look a bit weird.
        // Also, For future consideration: Do we want to allow customers to specify multiple extra CA's to handle server migrations?

        // If the CA is in the windows trusted roots, this will return true and chainStatus will be empty
        // If the CA is not in the windows trusted roots, this will return false and chainStatus will contain UntrustedRoot. Either are acceptable
        var chainOk = chain.Build(certificate);
        var chainOkUntrustedRoot = chain.ChainStatus.Length == 1 && chain.ChainStatus[0].Status == X509ChainStatusFlags.UntrustedRoot;

        // PROVIDED the chain terminates in the correct CA, and not some other CA that might randomly be in the windows trusted roots
        var chainTerminatesInCorrectCA = chain.ChainElements[chain.ChainElements.Count - 1].Certificate.Thumbprint == caCertificate.Thumbprint;

        if (chainTerminatesInCorrectCA && (chainOk || chainOkUntrustedRoot))
        {
            return true;
        }

        log.LogError("Server Certificate rejected due to invalid trust chain. Info: {0}", string.Join("", chain.ChainStatus.Select(c => c.StatusInformation))); // statusInformation has \r\n at the end itself
        return false;
    }
};

var client = new HttpClient(handler) {
    BaseAddress = new Uri(host)
};

The above code will pin a specific custom Certificate Authority. It's useful for internal deployments where you might have an internal certificate authority provided by your IT department, and you
want to ONLY trust certificates issued by that authority (and not random other certificates from LetsEncrypt, etc)

If you want to just pin the certificate itself then you don't need to verify the chain or anything like that.

Thursday, October 25, 2018

Native Mobile apps for iOS and Android; Writing it twice doesn't have to cost 2x

This post inspired by René Wiersma on Twitter:



As (I think I have) mentioned previously, my team is responsible for the company's Mobile Applications (and supporting cloud/on-premise services they connect to).

We need to support both iOS and Android as equals - in almost all cases we'll ship new features for both iOS and Android simultaneously, so the problem of developing for both of these different platforms keeps coming up; we've been doing this for the last 4-5 years.

We're not using any cross platform tools, so how do we do it? Simple, we write the app twice - once for each platform. For iOS we use Xcode and Swift (older code being ObjC) and for Android we use Android Studio and Kotlin (older code being Java).

Note: a key overall goal here from day one has been to try keep the code structurally similar, so that when you're fixing a bug on one platform, it's easy to find the corresponding piece of code to fix on the other platform. There is a tension here though against using the different abilities of each platform.

Original plan: One developer creates a major feature for a platform, another creates that same major feature for the other platform in parallel

Note: When I say "whole feature" I'm talking about a significant chunk of work - at least a month's worth.

We thought at the time that this would work, and perhaps the following might happen:
  • Each developer could work (and up-skill) on their preferred platform
  • Devs could work for a longer time without context-switching which would be good for productivity
  • We thought this would be OK as each dev would code-review the other's code, and they would both be involved in design reviews.

It was awful. I felt it took more than 2xtime/cost and generated technical debt like crazy..
  • Both devs would arrive at the same problems, and both would come up with designs to solve them. Usually these designs were different. This caused a whole lot of discussion and back-and-forth arriving at a consensus. The thing was - in almost all cases either design would have worked fine, and this discussion wasn't ultimately productive.
  • The devs didn't code review each-other as strictly as perhaps they should have, and when they did, their code had diverged enough that they simply reviewed it for correctness and didn't want to spend additional time going back over it and bringing it into line with the other platform.
  • And besides - if we need to align two diverged code-bases, whose code is the right code to align with?

Next plan: One dev creates a minor feature for a platform, then another dev does it for the other platform on a delayed basis; the feature is mostly complete by the time the second dev starts

We sort of fell into this due to devs coming free from other tasks at different times, but after the previous bad experience, we decided to make a deliberate go at this.
Note: our "minor feature" here is about 2-3 weeks or more. We simply could not use the delayed approach at the "major feature" level as it would have added so much time to the final delivery date that we wouldn't have been able to ship both platforms together on time.

This was only a little better. I felt it took about 2x time/cost
  • We thought the delay would prevent the "two people designing the same thing at the same time" problem and be more efficient. This did turn out to be true, BUT
  • The second developer burned a significant amount of time having to go back and learn how the first developer had done everything
  • Because the second developer didn't have the original context, when they had to make a different choice due to a platform difference, they didn't fully understand the code/design and would be more likely to make bad choices
  • The second developer ended up writing structurally quite different code just due to personal preference, which is a nasty trap for maintenance in future
  • This is terrible for testing. It makes sense for a tester to pick up the feature on both platforms at the same time, so they can check for consistent behaviour, etc.

Take 3: One developer creates a small sub feature for a platform, then another developer does it for the other platform after the first is pretty much done

The idea here was to try and minimise the bad effects caused by the large delay of the previous approach. We aimed for sub features being around 3 days or less worth of work, but sometimes up to a week.

This was better again, but still not great. I felt it had about 1.8x time/cost
  • I wasn't sure what would happen with designs, but it turns out even a small delay stops two people clashing trying to design the same thing in different ways, which was good.
  • The smaller delay meant that the second developer didn't have as large a chunk of work to catch up on
  • When the second developer needed the original context, it was relatively fresh in the mind of the first dev, so better discussions could be had
  • Testing waited til the second dev finished, but that was usually only a couple of days
  • BUT, the second dev still doesn't fully understand the problem in the same way the first dev did, so they still burn time coming up to speed constantly
  • ALSO, the second dev ends up using a different style/structure which hurts maintenance down the line. This is a big deal

Take 4: One developer creates a small sub feature for a platform, then that same developer creates it again for the other platform.

The idea was to try address the "second dev missing context and coming up to speed" problem.
Now we're getting somewhere. I feel this is about 1.5x cost overall
  • This means all the devs must learn to work on both platforms. If you're the kind of place that wants specialist iOS-only and Android-only developers you may not like this. Personally, I think pigeonholing people like that is terrible in any environment (Don't get me started on frontend vs backend developers), but hey, my team is small - perhaps when you have 50 devs you need to silo them like that, who am I to say?
  • This *is* effective; if it's the same dev doing both platforms, then they inherently understand the problem equally both times, and nobody else has to second-guess their decisions. Seems obvious, doesn't it.
  • BUT, when the dev would do the feature for the second platform, they would sometimes structure their code differently out of trying to better fit the other language/platform, or simply because it was a different week and they felt differently. We still had the maintenance pain down the line. Ouch.

Fifth time lucky: One developer creates a small sub feature for a platform, then that same developer ports the code in as literal a way as possible.

Goal: Stop the code diverging as much due to the devs being creative when writing the feature for the second.

Process: Write your small sub-feature for a platform, then open File.swift on one monitor and File.kt on the other, and go through function-by-function and line-by-line; Copy paste and then fix the errors and tweak the code.
The code inside each function doesn't have to match perfectly, but structural similarity is key here.

This takes discipline, but it is the best way we have discovered so far. It feels like it gets the cost/time down to about 1.3x
  • To quote a team member: "Porting code is boring", so that's a downside
  • Boring though it may be, porting code like this is really fast and often easy and safe. Things don't get missed nearly as much.
  • MAJOR Upside: If your code is in the same-named files, with the same-named methods in the same order, then when you make a fix on one platform it's very very easy to make that exact change safely on the other platform
  • Kotlin and Swift are much more similar than ObjC and Java, so now that we've moved to the newer languages this process is a lot easier than it used to be.

Conclusion

So here you have the recipe:
  • Break your tasks down into as small as possible sub-features
  • Have ONE dev do the sub-feature on one platform
  • Have the SAME dev do a "boring" port of that code over to the other platform. Don't be clever about it!
  • Enjoy :-)
Personally, I feel this is quite good. I've spent a good chunk of time using Xamarin and while the "porting code" cost is zero there, it has it's own problems and overheads. I feel like writing your app in Xamarin is about a 1.2x cost compared to writing a single-platform app in either native Swift or Kotlin, so to be able to write both native swift and kotlin apps with a 1.3x overhead seems pretty darn good to me! Note: the cost ratios are purely subjective based on my experience writing code in these various ways, and observing other team-members doing it too.
Note: We have definitely looked at various cross-platform tools, but found them all lacking:
  • Web-based (either PWA or cordova-type): Seriously evaluated and for what we wanted, we couldn't get good enough performance and nice enough UI
  • Xamarin: Seriously evaluated and found it to be really buggy, and also quite an additional burden to learn not only how the UI works (UITableViewController etc) but the quirks and oddities of how those things project into C#.
  • Flutter: Played around - maybe promising in future, but I think it's too early to depend on yet
  • React Native: Only read about it; Personally I can't get past the JavaScript. Ugh. Some people say that even for a single-platform app react native is more productive than Swift or Kotlin, and if this is true (while still maintaining the level of quality and performance we want) then it could be very compelling. It's on the list to investigate soon

Monday, October 01, 2018

Beware of locking helper functions in swift

If, like me, you are a swift programmer with a background in basically any other programming language, you'll be familiar with the lock statement.

C# Example:

class MyClass {
    private bool m_initialised = false;
    private int m_value = 0;

    void Initialise() {
        lock(this) { // lock stops two threads initialising at the same time
            if(m_initialised) {
                return; // already initialised
            }

            m_value = LoadExpensiveValue();
            m_initialised = true;
        }
        Console.WriteLine("initialised");
    }
}

Java has synchronized, heck even Objective-C has @synchronized - but swift has no such thing.

If you google for it, you most probably wind up on stack overflow, probably at this article: What is the Swift equivalent to Objective-C's “@synchronized”?.

It boils down to the following

1. Use objc_sync_enter to lock
2. Use objc_sync_exit to unlock
3. Don't forget to use defer to make sure the unlock always happens

Now, if you simply do the above, it's fine, however you (like me) are most probably tempted by that answer on that stack overflow post, which shows that you can write your own synchronised/lock function. Once you apply all the bells and whistles, you get something like this

Swift:

func lock(lockObj: AnyObject, closure: () throws ->T ) rethrows -> T
{
  objc_sync_enter(lockObj)
  defer { objc_sync_exit(lockObj) }
  return try closure()
}

And you can use it like this!

Swift Example:

class MyClass {
    private var _initialised = false
    private var _value = 0

    func initialise() {
        lock(self) { // lock stops two threads initialising at the same time
            if(_initialised) {
                return // already initialised
            }

            _value = loadExpensiveValue();
            _initialised = true
        }
        print("initialised");
    }
}

How cool is that? Swift didn't have a lock keyword, but we were able to write one ourselves, and it's just as nice to use as the native ones from C#, Java or anywhere else! Hooray!

Unfortunately, it is also wrong and broken

The above code compiles with zero errors or warnings, and there are many cases in which you'll never realise there's a bug there. I was bitten by this exact thing just today, hence my motivation to write this post.

The bug is, in the swift implementation, the print("initialised"); runs when _initialised is true; If two threads call the function, they will *both* print it.

Why? In swift, our lock keyword isn't a real keyword. It is a function which is being passed a closure.
That is to say, the locked region is a closure, and not part of the outer function.

The return statement for the "already initialised" case returns from the closure, but does not return from the outer initialise() function itself. This is in contrast to languages with a real lock keyword, which does not introduce a new return scope, and where the return statement actually returns from the outer function.

My advice: Do not write or use this handy swift lock function. One day you too will write this bug, and when it occurs it's not at all obvious as to what's going on.

Do this instead.


class MyClass {
    private var _initialised = false
    private var _value = 0

    func initialise() {
        do {
            objc_sync_enter(self); defer{ objc_sync_exit(self) }

            if(_initialised) {
                return // already initialised
            }

            _value = loadExpensiveValue();
            _initialised = true
        }
        print("initialised");
    }
}

The code is very slightly longer and very slightly uglier, however there are no closures, and the return keyword works how you expect it to.

Perhaps one day swift will add a proper lock keyword and we can all move on from this kind of thing