(If nothing else, this posting is bound to make me eligible for one of these lovely mugs. ;)

Has the file I installed on the far-away (or out-of-my-control) server been modified since I placed it there? Good question. If I'd kept a hash of the file at the time I created it, I could easily compare that to the file's current hash to find out. But I do tend to forget to do that…

You know the Spiel: you install a program with one or more supporting configuration files somewhere, and somebody calls you that "it" doesn't work. "Did you change something?" you ask. "No", comes the answer, but because you don't believe the answer, you tell "them" to use md5sum or sha1sum and you start comparing MD5 or SHA-nnn hashes.

What if your program could determine whether its supporting file(s) have been changed since you delivered them? What, even, if your program could refuse to use the file(s) if that occurred? This becomes more important if supporting files are scripts, say, Lua or maybe Python which, if changed, could wreak havoc. Imagine your package provides special DNSSEC trust anchors (or any other keys) in external files and these are replaced: how will your application prove that?

Code-signing comes to mind … (yuck) … but that's easier said than done. There have been some attempts at bringing code-signing to Linux, but I believe most have petered out into nothingness. Today, typical methods for signing files or program packages in a Unix/Linux environment include the use of GnuPGP signatures, signed MD5 or SHA files, unsigned MD5 hashes, and no form of verification at all.

I've been known for doing some (*cough*) curious things with DNS so I thought: well, why not leverage the trust obtained through DNSSEC to implement a simple form of "file-verification"? The operative word is "simple": the verifier must be able to work on the lowliest of CPUs -- I don't want a lot of number-crunching going on.

The scheme I've come up with is as follows:

A file's signature is created by obtaining its git hash which is basically the SHA-1 of the file's content plus a header which includes the file's size. The hex SHA-1 digest becomes the owner name (i.e. domain name) of a TXT record containing the object's file name in rdata. For example, the git hash of a file containing the 12 bytes of the text in file reference.file is: 557db03de997c86a4a028e1ebd3a1ceb225be238.

$ cat reference.file
Hello World
$ git hash-object --no-filters reference.file 
557db03de997c86a4a028e1ebd3a1ceb225be238

Queried directly in the DNS, the result (RRSIGs omitted) would be:

$ dig +dnssec 557db03de997c86a4a028e1ebd3a1ceb225be238.file.hes.jpmens.org txt
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 42523
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 5, ADDITIONAL: 1

;; ANSWER SECTION:
557db03de997c86a4a028e1ebd3a1ceb225be238.file.hes.jpmens.org. 86400 IN TXT "reference.file"

There's only a "reverse"-type lookup from SHA-1 to file name; a forward-type lookup (filename to hash) is not necessary.

Git's SHA-1 hashes are easy to create, and the makegitsha.py program outputs TXT records in DNS master zone file format; changing this to, say, update a PostgreSQL/MySQL database for PowerDNS or update the DNS dynamically is easy enough.

$ ./makegitsha.py  reference.file example.conf
557db03de997c86a4a028e1ebd3a1ceb225be238.file.hes.jpmens.org. 86400 IN  TXT "reference.file"
7193fa5175ab31c5502d96c50c81eb9e5b2ca513.file.hes.jpmens.org. 86400 IN  TXT "example.conf"

Programs can now query the DNS, preferably via DNSSEC, to verify that files they rely on haven't been tampered with. In order to do so a program will:

  1. Determine the file size and content and obtain their SHA-1 hash.
  2. Query the DNS for the owner name in a particular zone
  3. Verify the response

The program can, say, refuse to operate if it detects a configuration file's signature doesn't match that in the DNS. Heck, the program can even check its own signature and abort if it isn't verifiable.

  • Modified files are easy to deploy: re-hash them and publish their new signature. Existing (old) signatures can remain in the DNS to support previous versions (i.e. prior content).
  • I can even deploy "remote modifications" by having an administrator change a configuration file ("add this line") while I do an identical update and create the signature in the DNS.
  • Works with any DNS server (BIND, NSD, PowerDNS, etc.).
  • High TTL times on the "signature" records make for effective caching in recursive DNS resolvers.

I've created proof of concept as a set of C functions which implement file verification as discussed above. Basic usage is like this:

int main(int argc, char **argv)
{
    char *filename = argv[1];
    int rc;
    struct dvinfo *dvi;

    dvset_bits(DV_FORCE_AD);      /* Force DNSSEC check */

    dvi = dv_alloc(filename);

    rc = dv_valid(dvi);
    if (rc != F_SIG_OK) {
        /* fail */
    }

    /* file is valid */


    dv_free(dvi);
    return (rc);
}

Using the example program, I can demonstrate how this works. The program allocates a data structure (dv_alloc()) and tries to verify (dv_valid()) a file called reference.file, printing some of the elements of the opaque data structure (struct dvinfo *).

  • The file is signed and valid. (DNS response is NOERROR.)
$ ./example reference.file
    filename....: reference.file
    sha1........: 557db03de997c86a4a028e1ebd3a1ceb225be238
    ttl.........: 70900
    rdata.......: reference.file
    reason......: NOERROR
file `reference.file' is valid
  • Using the same data by a different name returns bad-signature because the SHA-1 hash is in the DNS, but the rdata contains an incorrect name. (I'm not sure a test for this is useful, though.)
$ cp reference.file reference.bad
$ ./example reference.bad 
    filename....: reference.bad
    sha1........: 557db03de997c86a4a028e1ebd3a1ceb225be238
    ttl.........: 70856
    rdata.......: reference.file
    reason......: NOERROR
file `reference.bad' signature-state BAD (githash in DNS but filename not in rdata)
  • After altering the reference file, it's marked as NOT valid because the file's SHA-1 hash isn't in the DNS (NXDOMAIN).
$ echo h >> reference.file
$ ./example reference.file
    filename....: reference.file
    sha1........: c9477886d6d694b1b6dc17bfa04e4d81af0a1d6d
    ttl.........: 0
    rdata.......: 
    reason......: NXDOMAIN
file `reference.file' is NOT valid: NXDOMAIN
  • example.c contains an optional test for verifying itself. I activate that and modify the executable program example, after which the signature is obviously unavailable in the DNS -- the program aborts.
$ echo 'hello foo' >> example
$ strings - example | tail -1
hello foo

$ ./example reference.file
Program file ./example has been modified. ABORT

The method discussed above is not real code-signing: code-signing means I can trust an operating system to refuse to load a program that's been tampered with. This system doesn't provide that kind of security. If a malicious person modifies the executable program to, say, disable the file-checking function (I'm assuming it's possible to do that), it breaks the system.

Further reading:

Flattr this
DNS, DNSSEC, and git :: 31 Jul 2012 :: e-mail

Comments

blog comments powered by Disqus