Skip to content

Development Notes

Ryan Bolger edited this page Sep 12, 2019 · 8 revisions

Invoke-WebRequest vs Invoke-RestMethod

The ACME protocol relies on sending things like 'Replay-Nonce' and 'Location' in a response header rather than part of the response body. Invoke-RestMethod in Windows PowerShell 5.1 has no way of capturing response headers. So we need to use Invoke-WebRequest instead for now, grab the nonce from the Headers property, and manually parse the Content property with ConvertFrom-Json rather than having PowerShell do it for us.

PowerShell Core 6 has actually added -ResponseHeadersVariable which solves this problem. But dropping Windows PowerShell support completely isn't really an option yet. So we're left hoping they back-port the parameter or sufficient time passes that restricting people to Core is feasible.

BouncyCastle Dependency

When I originally started this project, I was really hoping to keep everything pure .NET with no binary library dependencies. When I was initially only working with account keys, it seemed doable. But as I continued towards more heavy X509 stuff like generating cert requests, exporting PFXs and generating PEMs, it became apparent that I'd have to start relying on the legacy COM Certificate Enrollment API which is a lot harder to deal with and would make the whole project harder to maintain.

I briefly contemplated trying to fill in the gaps by shell'ing out to built-in command line tools like certreq and certutil, but they're just not flexible enough to do what I needed. So I turned to what seems to be the most common free 3rd party crypto library, BouncyCastle. And while it's certainly not perfect (particularly in the documentation department for the c# version), it's good enough for now.

And it does look like the native .NET BCL will be getting better crypto-wise soon. There's already a CertificateRequest class in .NET Core 2.0 and .NET 4.7.2 which just (April 2018) released. So I apologize for adding a 2 MB dependency on what should be a 0.2 MB module. But I'll try to replace it with native class libraries as soon as I can.

P.S. Totally not trying to rag on BouncyCastle here. I can't imaging trying to maintain an open source crypto library, particularly one that works largely the same between two different languages. I'm hugely grateful to that team. I just wish I didn't have to spend so much time searching for examples and tracing the source in Visual Studio to figure out how to do stuff.

instdev.ps1 script throws Copy-Item error

It's a known PowerShell limitation that once a DLL has been loaded into the session's AppDomain, there's no way to unload it. So if you're working on the module and using the instdev.ps1 script to synchronize changes and reload, you'll get an error because it can't overwrite the in-use BouncyCastle DLL. This should be fine to ignore because they BouncyCastle library shouldn't be changing very often. The other option is to close the existing PowerShell session and start a new one.

PFX compatibility and CSP stuff

The PFX files currently being generated via BouncyCastle are pretty simple. They don't have any Microsoft specific attributes such as a CSP name or some of the other bag attributes you might see on a cert exported from the Windows Certificates snap-in. It may be necessary to add these (optionally?) in order to make the files usable with things like IAS PEAP, LDAPS, and maybe Exchange (reference).

I briefly played around with adding these in Export-CertPfx.ps1, but I couldn't figure out how to get BouncyCastle to add the "Microsoft Local Key set" attribute properly. I'm leaving the partial code here to potentially pick up later.

    # Create an AsymmetricKeyEntry from the private key so we can inject Microsoft specific
    # bag attributes that will be written to the PFX when it's exported so it imports more
    # easily on Windows.
    $bagAttrs = @{}
    # Add the appropriate CSP
    if ($key.Private -is [Org.BouncyCastle.Crypto.Parameters.ECPrivateKeyParameters]) {
        Write-Debug "Injecting Microsoft CSP Name: Microsoft Software Key Storage Provider"
        $bagAttrs.'1.3.6.1.4.1.311.17.1' = [Org.BouncyCastle.Asn1.DerBMPString]::new('Microsoft Software Key Storage Provider')
    } else {
        Write-Debug "Injecting Microsoft CSP Name: Microsoft Enhanced Cryptographic Provider v1.0"
        $bagAttrs.'1.3.6.1.4.1.311.17.1' = [Org.BouncyCastle.Asn1.DerBMPString]::new('Microsoft Enhanced Cryptographic Provider v1.0')
        #$bagAttrs.'1.3.6.1.4.1.311.17.2' = [Org.BouncyCastle.Asn1.DerOctetString]::new('')
        #$bagAttrs.'1.3.6.1.4.1.311.17.2' = [Org.BouncyCastle.Asn1.DerOctetString]::new(0)
        #$bagAttrs.'1.3.6.1.4.1.311.17.2' = [Org.BouncyCastle.Asn1.Asn1Object]::FromByteArray([byte[]]@(4,1,0x82))
        #$bagAttrs.'1.3.6.1.4.1.311.17.2' = [Org.BouncyCastle.Asn1.Asn1EncodableVector]::new()
    }
    $keyEntry = [Org.BouncyCastle.Pkcs.AsymmetricKeyEntry]::new($key.Private, $bagAttrs)

The raw hex in a "good" file has the following sequence 30 0D 06 09 2B 06 01 04 01 82 37 11 02 31 00 which breaks down like this:

  • 30 = Sequence 0x10 + Constructed 0x20
  • 0D = length 13
  • 06 09 2B 06 01 04 01 82 37 11 02 = The '1.3.6.1.4.1.311.17.2' OID in hex
  • 31 = Set 0x11 + Constructed 0x20
  • 00 = length 0