Include digital signatures on project packages
I propose to implement MD5 checksum for each packaged project.
It can be provided as a part of the title.
We need to use it via project installer to make sure that package is not hacked and replaced.
Advocates: @jlfranklin @quicksketch
Weekly update?
~Proof of concept by quicksketch: https://github.com/backdrop-ops/backdropcms.org/pull/456~ Initial core PR by @jlfranklin: https://github.com/backdrop/backdrop/pull/2449
Other related issues that also need to be done:
- [x] Project Module API improvements: https://github.com/backdrop-contrib/project/issues/25
- [ ] Project Module Code Sign Support: https://github.com/backdrop-contrib/project/issues/29
- [ ] Update Installer Module: https://github.com/backdrop/backdrop-issues/issues/3714
Nice example is here: https://github.com/syncthing/syncthing/releases/
👍 Security++
👍
Ooh. SyncThing also GPG signs their releases.
MD5, while good for a lot of things, has been weakened enough that it shouldn't be used for making sure a package hasn't been intentionally hacked.
I like the idea, but I suggest a stronger hash algorithm... SHA256, maybe. Even better, GPG sign them or encourage module maintainers to do so.
Retitling this slightly. Like @jlfranklin, I think we can do better than md5, but if want to start with a simple hash and expand it out later, we can do that.
Research over in #2018 led me to https://paragonie.com/blog/2016/10/guide-automatic-security-updates-for-php-developers, which included many, many excellent ideas. One of my favorite ideas is using private/public keys for digitally signing the packages. Backdrop's packager would sign the packages, which would then be stored on GitHub as they are now. Backdrop core would include the public key for validating the packages.
The message of the package could even then be the md5 of the zip archive. So you'd both verify that the package came from a legitimate location, and that the zip file had not been tampered with in transit.
For the public/private keys, the author recommends using libsodium (https://download.libsodium.org/doc/) which is built into PHP 7.2 and higher (if included in the compilation). But as we support many older versions of PHP and even new installations might not have it built in, the author amazingly created a bundleable version of libsodium as a pure PHP package that can be included as a .phar file (660KB): https://github.com/paragonie/sodium_compat
Perhaps that's too much to handle all in one go, or perhaps not. If we're going through the work of updating the XML feeds on BackdropCMS.org to include an MD5, it could nearly as easily include a digital signature instead.
...GPG sign them or encourage module maintainers to do so.
If our packager can do that for them, then we should do that for them :) ...basically the plan that @quicksketch has laid out.
I've been making progress on this functionality. Adding MD5 capability to the Project Release module is fairly trivial. So that shouldn't be difficult at all.
Digital signing also doesn't appear to be too difficult, though we need to make some architecture decisions.
Using the Sodium Compat library at https://github.com/paragonie/sodium_compat, the basics of signing are provided in the README.md file:
<?php
require_once "/path/to/sodium_compat/autoload.php";
$alice_kp = sodium_crypto_sign_keypair();
$alice_sk = sodium_crypto_sign_secretkey($alice_kp);
$alice_pk = sodium_crypto_sign_publickey($alice_kp);
$message = 'This is a test message.';
$signature = sodium_crypto_sign_detached($message, $alice_sk);
if (sodium_crypto_sign_verify_detached($signature, $message, $alice_pk)) {
echo 'OK', PHP_EOL;
} else {
throw new Exception('Invalid signature');
}
Applying this to Backdrop would mean the following:
- We would generate a key-pair per the above code.
- The private key would be stored in a locked-down location on backdropcms.org server.
- The public key would be bundled directly into Backdrop core, probably under
/core/misc/keysor in/core/modules/installer. - When the packager runs on backdropcms.org, it would md5 hash the archive file, and store this md5 value in the
project_releasetable. - After doing the md5, the packager would also create a digital signature, using
$signature = sodium_crypto_sign_detached($message, $secret_key). The "message" would be the md5 value of the archive. It would also store this signature in theproject_releasetable. - Both the MD5 and the signature will be added to the project XML feed, such as the one at https://updates.backdropcms.org/release-history/backdrop/1.x
- When the Installer module downloads a new module or release, it will first validate the MD5 of the archive. Then it will take the signature value, and decode it with
sodium_crypto_sign_verify_detached($signature, $message, $alice_pk). Where the $message is the MD5 string again, and the public key is the one bundled with Backdrop core. - Now we know that the package both has not been tampered with, and it was packaged exclusively through the private key that only exists on the BackdropCMS.org server.
Some additional thoughts:
- We should probably create not just one, but two key pairs. The first private key is stored on backdropcms.org. The second private key is a backup we would store only in an offline location. Both public keys would be bundled with Backdrop core. The backup would be used in the event the main private key becomes compromised, in which case we would regenerate all the signatures using the new key.
sodium_crypto_sign_detached()creates a string that is made up of random characters, so it doesn't seem to have any real sane representation when viewed in a text file. Here's what a public key looks like:
çhú=!M/Åv¼{D½˜—æ>þœ¯Èfçüð·N-ðµ˜B-ÀÂÙæCØs(Œé‘4/CPÜ¡
ĆþLµ˜B-ÀÂÙæCØs(Œé‘4/CPÜ¡
ĆþL
- As this string is likely to be interpretted incorrectly when transferred as text, we should encode it in some way before we store it. The AirShip CMS made by Paragonie provides some example as to how to do this, where they use base64 URL-safe encoding before storing and writing keys to disk.
In \ParagonIE\Halite\Asymmetric\Crypto::sign, the code to do the actual signing is:
public static function sign(
string $message,
SignatureSecretKey $privateKey,
$encoding = Halite::ENCODE_BASE64URLSAFE
): string {
$signed = \sodium_crypto_sign_detached(
$message,
$privateKey->getRawKeyMaterial()
);
$encoder = Halite::chooseEncoder($encoding);
if ($encoder) {
return (string) $encoder($signed);
}
return (string) $signed;
}
Where $message is an MD5 string, and $encoding is "base64urlsafe". They provide the option to use a different encoding, but I think for practical purposes, we can just assume a Base64 encoding in the BackdropCMS.org packager. I don't think we need to worry about making the encoded version URL-safe, but Backdrop does include a backdrop_base64_encode() function for URL-safe strings, but it does not have an equivalent function for decoding them.
Sounds good.
Sounds like you're reinventing SSL signing, minus the identity features.
Where you have an offline key, SSL would use a CA key and cert. (It's easy enough to create a CA cert using OpenSSL.) The CA's key (private part) is kept offline, the cert itself (public part) is baked into Backdrop. A signing key is generated and that key/cert lives on backdropcms.org for signing packages. (Actually, it should live in a more secure signing server that has limited access to the internet, and only listens for signing requests from whitelisted sites like backdropcms.org.)
The signing cert can be recreated if (when) it is compromised, and the individual package certs can carry the identity of the package and author(?) or signer(?). The date in the signed package's cert should match the release date of the module, and if it isn't refreshed in n years when the cert expires, the module should be considered abandoned.
Separate signing certs can be generated based on characteristics of the module. For example, regular modules would get signed by the main cert, sandbox modules would be signed with a lower-grade cert.
Using a certificate revocation list, modules with (>= serious) security releases have their signing certs revoked, as will modules that are considered abandoned before their cert expires.
There will need to be some work done to verify the CA cert is the one that signed the package, but that code exists.
The Backdrop project could then sell signing certs to major web shops or (cough) forks of the project as a way to generate some revenue.
Sounds like you're reinventing SSL signing, minus the identity features.
Well, I'm not reinventing SSL signing, I'm just using libsodium key-pair signing instead of SSL certificate signing.
You bring up a good point though, we might be able use OpenSSL signed certificates instead of Sodium signing. I could see benefits and downsides: The existing Sodium functions seem to be made for this exact purpose, they're fairly trivial to implement, are (supposedly) quite secure, and are built into newer versions of PHP. On the downside, this technology is relatively new, and requires an backwards compatibility shim for older PHP versions.
OpenSSL is available for all versions of PHP, but it requires that OpenSSL be installed on the host computer and PHP configured to use it correctly. I would expect that having PHP correctly configured and --with-openssl compilation flag might turn into a common installation problem.
I'm also not clear on how an OpenSSL implementation would work. @jlfranklin could you attempt to stub out code you would expect to validate the signature of a downloaded package?
The Backdrop project could then sell signing certs to major web shops
They would only need signing certs if they were redistributing their modules through another means than backdropcms.org, because the packager will already do the signing for everything in the contrib group. At this point we're just looking to make sure auto-updates come from a valid authority (backdropcms.org), so this validation only occurs when downloading a module, we wouldn't check anything for modules that already exist on disk (i.e. custom modules).
Overall on the question of OpenSSL vs Sodium (or any other option), I'm fine with any of them as long as it's secure and relatively simple. I think we should choose whichever solution is going to be the least hassle for inexperienced end-users.
I'm also not clear on how an OpenSSL implementation would work. @jlfranklin could you attempt to stub out code you would expect to validate the signature of a downloaded package?
I'd be happy to. Give me through the weekend. I'm not sure how much free time I have this week.
I haven't tested all the pieces together, but I've put up a proof of concept against the backdropcms.org repository: https://github.com/backdrop-ops/backdropcms.org/pull/456
For the time being I've just written it to use the sodium functions (though I have not included the sodium library itself yet). So far I've only tested creating a release and verifying that the MD5 is added correctly and that the MD5 + signature columns are added to the project_release table. Alternatively, we could put the signature as an attachment (asset) on the GitHub release, but it will make invalidating/regenerating signatures much easier if they're all stored centrally on backdropcms.org.
I would expect that having PHP correctly configured and
--with-opensslcompilation flag might turn into a common installation problem.
I am sharing the same concern.
The Backdrop project could then sell signing certs to major web shops or (cough) forks of the project as a way to generate some revenue.
They would only need signing certs if they were redistributing their modules through another means than backdropcms.org
Sorry @jlfranklin, I am strongly against anything like this. What makes sense to me the way @quicksketch puts it, is that if a 3rd party is distributing their custom modules from another source, then perhaps we should be making profit out of that. Rationale:
- b.org contrib-land is free and has low/no-barrier to enter, so people can bring their code there for distribution.
- I see no substantial reason for one to use another source, unless they plan to make money out of it (paid-for modules/themes)
- If people are making money directly from the product our community is volunteering time/energy/love to build, then at the very least b.org deserves a "donation" 😄...which could be used to cover our expenses and/or boost our ~marketing~ ahem, "outreach" ... efforts.
my 2c
My vote would be for using Sodium because 1) I like things that are easy, for learnability reasons. and 2) the shim may be useful for servers that don't have the Sodium module compiled with PHP, even after version 7.2, and 3) since we've already started going down this road this approach may also be the shortest route to the finish line. my .02 ;)
I've worked through enough of the coding that I think I have a workable architecture for SSL-based signatures on modules. This post is pretty dense and technical. I don't recommend starting unless you have some time and spare cycles to devote to it.
Before we start, I want to emphasize that SSL is about identity, not encryption. Encryption is a prerequisite for SSL, not a benefit. The same encryption algorithms used by SSL are also used by SSH. You could use an SSH tunnel to connect to a website securely, but you would have no assurance you're talking to the right web site. SSL provides the identity mechanism to know who you're talking to.
Each SSL cert is a document that says, "I [insert signer's name here], hereby certify [insert subject's name here] is the owner of the private key matching the public key in this cert," and a digital signature that can be verified with the signer's public key. The cert contains the Common Name or CN of both the subject's name and the signer's name. (If you recognize CN from LDAP, it's because SSL and LDAP both derive from X.500 and share a lot of the same terminology.)
SSL certs define a one-to-one, "A signs B" relationship. If you need multiple entities to sign a key, you need multiple certs.
When I talk about creating certs below, I implicitly include the creation of a public-private key pair for the cert.
With all that in mind, let's begin.
As an overview, below is the certificate chain that I envision, with the CN of each cert in parenthesis.
Root CA Cert (Backdrop Codesign CA) signs Class n Signing Cert (Backdrop Codesign Intermediate Class n) signs Developer Signing Cert (Edward Xample, Developer) signs Module Signing Cert (My Module, v1.0) creates signature block (encrypted digest of some hash).
The Root CA Cert, Backdrop Codesign Intermediate cert, and any other CA certs are kept in core/misc/certs. The Root CA's private key should be kept offline in a secure location, such as a bank deposit box. The Codesign private key is on a hardend server that listens for signing requests from Backdrop.org and other whitelisted servers.
The developer applies for his signing cert from the Backdrop Project. For the core code and core modules, the developer cert will have something like "Backdrop Core Team" as the CN, and be managed by the release team.
Each module has a MANIFEST.txt file with the file names of all module files, including submodules. I considered using the module's .info file to store the manifest, but decided against it as module packages that include submodules would have multiple info files, and I thought the signature should be for the full module package. If the MANIFEST.txt isn't created by the author, it should be created by the signing process.
Now that the developer has his own signing cert, we're ready to sign a module.
The signing process creates a module cert signed by the developer's cert. The name of the module and its version is the CN, and the subjectAlternativeName has the machine names and versions of the modules and submodules. (E.g., my_module-1.0, my_submodule-1.0.) The subjectAlternativeName allows Backdrop to easily match the modules to the signatures.
The signing process reads the manifest file and concatenates the contents of each file named in order, finally adding the manifest itself to the end. This is the "data" that will be signed.
The signing process then uses the private key associated with the module cert to sign the module "data". The signature -- a hash of the "data", encrypted with the signer's private key -- is written to a codesign.pem file in a format that saves the hashing algorithm (e.g. sha256) and the key ID of the module's signing key. The module's cert, the developer's cert, and any other intermediate certs are added to the codesign.pem file.
When Backdrop attempts to verify the signature, it reads the codesign.pem file to get the signature block and the cert with the public key needed to verify the signature. If the signature checks out, the verification process follows the cert chain back until it finds one of the CA certs (success) or the chain is broken (failure.)
Multiple signatures can be in the codesign.pem file. A second signature from the "Backdrop Module Publishing Service" can be present in codesign.pem, with the Publishing Service replacing the developer's cert in the chain.
Finally, a Codesign module can add signing information to the admin/modules page or its own admin/reports/codesign page, and a cron hook to verify the code from time to time.
That's it.
So far, it's about 400 lines of PHP. With proper docs and tests, I expect it to be around 1,000 lines total. I've run into a couple dead ends, because some of the OpenSSL functions were introduced later than I thought, some as recently as 7.2, and I wanted to make sure we had a solution that would work with PHP 5.x.
Obviously, the code will need some thorough vetting, but I expect the hardest part will be the infrastructure to support it.
I would have loved to get this into 1.12, but there is no way that is going to happen. Here is a quick update on my progress.
To get this to work requires changes in multiple areas:
- A new core
codesignmodule to handle the crypto. - Changes to the module installer to use
codesignto verify the downloaded modules. - Changes to the Project module (specifically
project_release) to allow other modules to add data to a release. - An additional
project_codesignmodule to generate and add the signatures to module releases.
Changes to Project are already done. (See backdrop-contrib/project#25.)
The codesign module is mostly done, but I keep refactoring major pieces of it as I develop other parts of the system. (Track https://github.com/sd-backdrop/backdrop/commits/BD-1992 for progress.) The current codesign module is crypto-engine agnostic, allowing contrib modules to handle the signing and key management for OpenSSL, GnuPG, Sodium, or whatever the new hotness is next year.
I'm writing two contrib modules to ensure codesign is crypto-engine agnostic, one for OpenSSL, one for GnuPG. They are evolving with codesign.
The project_codesign module is my current focus. I'm debating adding it as a submodule to Project or creating a separate contrib module.
And, of course, there will need to be a suite of tests written for all the different parts.
Thanks for all your hard work and energy/time spent on this @jlfranklin 👍
Do we absolutely need to have a separate module for this? When we merged diff module in core (for use in the config diff in /admin/config/development/configuration), we have done that as an include file. I mean, is this something that we will need to turn on/off?
It implements hook_menu() and will likely have some of its own configuration and tables by the time it's done. Codesign is not required for a site to function properly, so it could be turned off.
Thanks for taking the time to respond @jlfranklin ...have not gotten my head around everything involved yet, but this helps.
See also https://github.com/sd-backdrop/codesign_gnupg as the first functional signing module.
Awesome @jlfranklin! I haven't checked in on this in a long time! It's great to see progress here.
I'm debating adding it as a submodule to Project or creating a separate contrib module.
Perhaps start separate, but if we use this on backdropcms.org we'll want it directly in project module. The situations where project module is not part of backdropcms.org are our secondary use-case, and it's already hard enough to manage with project module being separate from the main backdropcms.org repository.
Can I quibble about the name? I got confused by "codesign" and others will too. I first read it as "co-design". I suggest making it "code_sign" so it's clearer.
Makes perfect sense @herbdool 👍
I suggest making it "code_sign" so it's clearer.
No objection.
This has come far enough to generate code signatures, and therefore far enough to get some other people looking at it. This PR by itself is testable, but does not achieve the goal of the ticket. That requires some patches to the Project module (see backdrop-contrib/project#25 and backdrop-contrib/project#29) and adding the sd-backdrop/code_sign_gnupg or sd-backdrop/code_sign_openssl contrib modules.
Don't look at those links, yet. Let's go through this PR, first.
The PR adds a core/modules/code_sign module that, despite its name, is able to sign any data that can be passed in as a string to the signing function. The code_sign module cannot sign anything by itself, it requires a signing module to do the actual signing and any key management.
For demonstration purposes and to run the core Simpletest suite, a code_sign_hash module is included. It returns a simple hash of the data, using any hash supported by the PHP hash() function. This is good enough for testing, but not real world module signing.
- Build a test site with backdrop/backdrop#2449.
- Enable only the Code Sign module.
Once you've done the two bullets above, two new menu items that will appear in the site. One at Configuration => System => Code Sign, the other at Reports => Code Sign. The one under Configuration has just a dummy page where other modules can add their own config tabs. The report page notes no signing modules are installed and invites you to install the Hashing Code Sign module.
- Enable the Hashing Code Sign module.
Now the report page lists a profile and a new Hash Signing tab is under the Configuration => System => Code Sign section where hashing profiles can be managed.
Externally, that's it. The rest of the PR provides an interface for signing with a selected "profile" provided by a signing module, and verifying the signatures are valid. For that, we'll need a module that uses all that extra plumbing.
Have you done everything in the last comment? Feeling pretty good about it? Good, so am I. Let's move on to the Project module.
Here's a little background on Project, in case you haven't used it before. The Project module manages nodes for Projects and Project Releases. (Read: contrib modules and their releases.) This is used on b.o to create the searchable library of contrib modules that have full releases. Through hook_cron, it creates XML files with the metadata for the each project and for each release of each project. Backdrop sites download these XML files to generate the "Available Updates" report and the Project Installer module uses them for downloading the list of available modules and their latest versions, so you can install and upgrade modules without needing FTP access.
We want to add signatures to the XML, and to do so there are two patches to the Project module that need to be applied (but don't do it yet):
- Issue backdrop-contrib/project#25 changes how the XML is generated in Project.
- Issue backdrop-contrib/project#29 adds a
project_code_signmodule to optionally sign modules and put the signatures in the XML.
Before the patches, Project used a lot of string concatenation to generate the XML. This makes it difficult to add new elements into the XML, like the signature blocks. The first patch above creates a nested array with all the metadata, passes the array to one of three new alter hooks via backdrop_alter() (depending on the code path), and then uses format_xml_elements() to generate the XML itself.
The second patch adds the Project Code Sign Integration module and adds a "Regenerate Release XML" page under Configuration => Project. The new module generates signature blocks for the project release's zip files and inserts the signature into the XML via the new alter hooks introduced above. It also saves the signatures into a database table so that regenerating the XML doesn't mean resigning everything.
The patch in backdrop-contrib/project#25 is included in the backdrop-contrib/project#30 PR. You can pull just this PR and get both at once (Which is why I told you not to apply them both above.)
OK, now do this:
- Checkout the
1.x-1.xbranch of Project into the modules directory - Apply the PR backdrop-contrib/project#30
- Enable "Project Code Sign Integration" and any dependencies it wants.
- Create a Project node for testing.
- Create two Project Release nodes for the Project node. The "download link" fields have to point to a real file that your test web site can reach. The rest of those three nodes can be test data.
We have everything we need to start signing modules. The Project Code Sign Integration module adds a tab at Configuration => Project => Codesign to manage how modules are signed. You should see the Default hash profile listed there. If you go back over to the Hash Settings tab from the previous comment, you can add new Hash profiles and they'll all appear in the Configuration => Project => Codesign page. Let's do that.
- Go to Configuration => System => Code Sign => Hash Signing
- Add a couple new Hash Signature Profiles:
- Weak with MD2
- Strong with SHA512
- Go to Configuration => Project => Codesign
You should see all three Hash signing profiles now: Default, Weak, and Strong.
- Check all of the available profiles and save.
- Click the "Regenerate Release XML" tab.
There is an checkbox here to "Regenerate signatures." All it does is delete everything from the signature cache table so the XML regeneration process will have to create new signatures.
- Click the "Regenerate Release XML" button and wait for the batch to complete.
- Look in your
files/release-historydirectory.
There should be a directory for the project node you created above, and two XML files. Open up either one in your favorite text editor and you should see the following in each <release> block:
<signatures> <signature> <crypto_engine>hash</crypto_engine>
<profile_id>default</profile_id>
<signature_block>sha256:eba15d583a8b5ba4f7e1cfe39cf5e6b175b1da7fd38aa33003b981eef0702aed</signature_block>
</signature>
<signature> <crypto_engine>hash</crypto_engine>
<profile_id>strong</profile_id>
<signature_block>sha512:8b641585bbdd9281d8af4564375002fadb2c2d2e85e425801b2e7783914b296fce8967f62ba43aece9b5686c29a89c756fa8a0b5ed89406923cedd5c36a8d373</signature_block>
</signature>
<signature> <crypto_engine>hash</crypto_engine>
<profile_id>weak</profile_id>
<signature_block>md2:d62b90d16469c19374ed468570d227dd</signature_block>
</signature>
</signatures>
Congratulations. You've just signed your first module. (Or whatever test data you used.)
Perhaps start separate, but if we use this on backdropcms.org we'll want it directly in project module.
The project_code_sign module got added as a sub-module, mostly for patching convenience. Extracting it as its own contrib module is trivial, and there are no technical blockers to doing so.
After two long comments, we have hashes, but no signatures in the XML. To do so, we need to install a module that does some kind of PKI-based signing. There are two additional modules that can provide this.
Currently, the GnuPG module is a bit easier to use, so let's start with that one.
- Checkout the 1.x-1.x branch of Code Sign GnuPG into the modules directory.
You'll need to install gnupg into your system. If you're running Linux, this is as easy as installing any other package. For some popular distros, the following commands will install GnuPG:
- Debian or Ubuntu:
apt-get install gnupg - Fedora:
dnf install gnupg - Arch:
pacman -S gnupg
What we're about to do here is a basic setup. Going into the details of GnuPG features, the web-of-trust, and other details is outside the scope of this thread. This is also not the recommended way, because the key will lack a passphrase protecting it.
First, we'll need to create a key. The command gpg --gen-key will ask for a real name and an email address, and a passphrase. It will print out other stuff, but what you see below is the important part. When it asks for a passphrase, just hit Enter. GnuPG will advise against skipping the passphrase, do it anyway. (We will support passphrases when the module is finished.)
$ gpg --gen-key
Real name: Backdrop Test Signing Key
Email address: [email protected]
You selected this USER-ID:
"Backdrop Test Signing Key <[email protected]>"
The key is created and in your keychain. Now, we need to export it to a file so we can upload it to the website.
$ gpg --export-secret-keys > bdtest.gpg
If you didn't give it a passphrase above, it shouldn't ask for a passphrase now. If all goes well, there will be a bdtest.gpg file in the current directory.
- Go to Configuration => System => Code Sign => GnuPG => Import key
- Upload the bdtest.gpg file.
- Click the GnuPG => Settings subtab.
There should be one familiar looking option for the Default signing key. In Configuration => Project => Code Sign, it should show up as a signing option. If there isn't, then the web server probably can't write to its $HOME/.gnupg directory. It should look like this. (Replace www-data with the user name the web server runs as.)
$ ls -ld ~www-data/.gnupg
drwx------ 2 www-data root 4096 Mar 14 03:26 /var/www/.gnupg
- Go to Configuration => Project => Code Sign
- Enable the GnuPG signing profile and save.
- Regenerate Release XML, just like we did in the last long comment.
If the crypto gods are smiling on you, then your module's XML should include something like this:
<signature> <crypto_engine>gnupg</crypto_engine>
<profile_id>74D1063DAB2EE638AEEF31423B30EC9640CF7EA0</profile_id>
<signature_block>-----BEGIN PGP SIGNATURE-----
iQGzBAABCAAdFiEEdNEGPasu5jiu7zFCOzDslkDPfqAFAlyKAuMACgkQOzDslkDP
fqDXwwv/SREGkMzo1Iup5r8VoH5U3SWhp69Bwf5Nfq1y8Lx0f7ri66AVj8uWmQzs
LhpI2rV5EDU7uPKjs7Txetb2PTHDLpmgwB6VlBpsIr/omzEeTGVAfT/zlSv4lyhh
QbpyEBH9ccZV9pJ3hdd8bVzizjhm/yn1Igqrpi7QUekCfu7U53PV4r1UBJP068bM
tOvWeNoySlsz6JPOrTSvOJSDIlx+tSI/WCeY7rWSC3+b48e9FIZZVnu4HKnqPi6k
DC4HWdM6ioTI1Q29BfmFA2I2OjyAKkLaZ/8XJj/wdW+l5KvGU2rHxryk010+h/u/
zFv73nG8ODVz0tN5qAnYuPYqctXkxnAYgTVcbqZVR9ERHluuIkIr3fhKRNZVBa4U
0qGN9BAjtf3J6m3KSPZHAC/fCeXvtJfuA0yeF/jWd3pGj9owV+G6qNDt260JwpX5
UuttSVF9t5Y0Sk5Gkeehi8swoMK9FhddJ2QbNNOBwfChbOUJPJ931CqntHBhV+Ti
xsPlhWpf
=fgO+
-----END PGP SIGNATURE-----
</signature_block>
</signature>
Wow, this looks awesome so far. Thanks for the hard work!
Thanks @jlfranklin!!! I haven't yet had the time to review this. It sounds great though! I'll get to this when I can.