Using riffverify - rallytac/pub GitHub Wiki

Engage Recording Verification Tool

Recordings made by Engage are, by default, digitally signed to prevent against tampering. This is done using ECDSA based on contents of the recorded file. The resulting signature is stored inside the recorded file.

Now, ECDSA works it's magic using an X.509 certificate - specifically the public key of the certificate. So if you want to check that a recording has not been tampered with, you're going to need that specific certificate. Fortunately, you don't have to go hunting for it because Engage will automatically store the certificate in the recorded file as well. Cool huh!?

So, all you need to verify a recording is the actual recorded file and our handy-dandy riffverify utility.

This thing is called riffverify because Engage's recordings (at the least audio files) are stored in Resource Interchange File Format - aka known as RIFF - which is kinda like a little database consisting of various "chunks" of data. Those chunks mean different things to different applications - audio apps, for example, care about the RIFF, fmt , and data chunks in the file as that's the bare minimum they need to play audio. Engage adds more chunks named json, cert, and sig which respectively describe the metadata of the recording (like who the transmitter was, when it happened, which Engage node it came from, and so on), the public portion of the certificate used for signing the recording, and the actual signature.

Using riffverify

While riffverify can be run as an interactive tool to provide on-screen feedback on verification, it can also be used as part of a workflow where its invoked by other tools and applications that expect simple, succinct, feedback. This second option is know as quiet mode and is specificied with the --quiet command-line option.

Let's give this a whirl. Assume we have 2 recordings - good.wav and bad.wave. The good one is a signed, unmodified recording while the bad one is also signed and, subsequent to signing, has been tampered with (actually, just a single byte was modified in the file).

Interactive Operation

Invoking riffutil in interactive mode for the good file would look as follows:

$ ./riffverify good.wav

The output would look something like this:

---------------------------------------------------------------------------------
Engage RIFF Verification Tool version 1.233.9073 [RELEASE] for darwin_x64
Copyright (c) 2019 Rally Tactical Systems, Inc.
Build time: Dec 12 2022 @ 11:06:46
---------------------------------------------------------------------------------
1

Notice the 1 that is output. This means that the file has been verified and all is fine.

If we do the same for the bad file (the one where just a single byte has been changed in the file), we'd get a 0 as the output (and an error message).

$ ./riffverify bad.wav
---------------------------------------------------------------------------------
Engage RIFF Verification Tool version 1.233.9073 [RELEASE] for darwin_x64
Copyright (c) 2019 Rally Tactical Systems, Inc.
Build time: Dec 12 2022 @ 11:06:46
---------------------------------------------------------------------------------
2022-12-12 11:09:33.856 [5503/0x113acd600-                ] E/RiffUtils: 'bad.wav' failed signature verification!
0

Non-Interactive Operation

If you wanted to use riffverify as part of a workflow, you'd want to do things a little differently. First, the application invoking riffverify isn't going to care about the title stuff that riffverify prints out. Nor is it going to care about other messages coming from the underlying Engage Engine inside riffutil. So we'll make that nice and quiet, as follows:

$ ./riffverify good.wav --quiet

The output now is just the 1 value. Also, the process exit code will be 0 - which the invoking application should check in addition to reading the output to STDOUT (the 1 value).

1

But ... some operating systems may output some extra stuff to STDERR which we'd want to toss away too. So, we'll redirect messages to STDERR to the the Twilight Zone (/dev/null) as follows:

$ ./riffverify good.wav 2> /dev/null
1

Doing the same for the bad file, we'll get 0 as the on-screen output and the process exit code will be a non-zero value.

$ ./riffverify bad.wav --quiet 2> /dev/null
0

Seeing More Stuff

How riffverify works it to call an API function inside it's Engage Engine to do the work of verification. What comes back to riffverify is a whole lot of goop (called JSON) which describes the RIFF file. Part of that description is a field named verified which riffverify returns as 1 or 0 depending on whether that value is true or false. Now, seeing that you're likely normally simply interested in whether the RIFF is verified or not, riffverify doesn't send back all that JSON for you to dig around in.

But ... if you're one of those fun-loving techie types who lives on the edge (...of your Aeron chair, frankly); you may be interested in all that fun JSON stuff. So, you can tell riffverify to give it to you by specifying --descriptor on the command line, as follows:

$ ./riffverify good.wav --quiet --descriptor 2> /dev/null

That'll give you something like this:

{"certDescriptor":{"fingerprint":"7D:1A:6C:E6:C0:EC:53:1C:9D:99:20:48:81:E3:C3:87:43:31:C3:26","issuer":"/C=US/ST=Washington/L=Seattle/O=Rally Tactical Systems, Inc./OU=(c) 2019 Rally Tactical Systems, Inc. - For authorized use only/CN=Rally Tactical Systems Root CA 1/[email protected]","notAfter":"Sep  4 01:53:06 2029 GMT","notBefore":"Sep  7 01:53:06 2019 GMT","selfSigned":false,"serial":"AD:CB:61:C8:99:4E:21:E1","subject":"/C=US/ST=Washington/L=Seattle/O=Rally Tactical Systems, Inc./OU=(c) 2019 Rally Tactical Systems, Inc. - For authorized use only/CN=Engage Factory Default Certificate/[email protected]","version":1},"certPem":"-----BEGIN CERTIFICATE-----\nMIIDZjCCAscCCQCty2HImU4h4TAKBggqhkjOPQQDAjCB9TELMAkGA1UEBhMCVVMx\nEz
ARBgNVBAgMCldhc2hpbmd0b24xEDAOBgNVBAcMB1NlYXR0bGUxJTAjBgNVBAoM\nHFJhbGx5IFRhY3RpY2FsIFN5c3RlbXMsIEluYy4xSDBGBgNVBAsMPyhjKSAyMDE5\nIFJhbGx5IFRhY3RpY2FsIFN5c3RlbXMsIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1\nc2Ugb25seTEpMCcGA1UEAwwgUmFsbHkgVGFjdGljYWwgU3lzdGVtcyBSb29
0IENB\nIDExIzAhBgkqhkiG9w0BCQEWFHN1cHBvcnRAcmFsbHl0YWMuY29tMB4XDTE5MDkw\nNzAxNTMwNloXDTI5MDkwNDAxNTMwNlowgfcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI\nDApXYXNoaW5ndG9uMRAwDgYDVQQHDAdTZWF0dGxlMSUwIwYDVQQKDBxSYWxseSBU\nYWN0a
WNhbCBTeXN0ZW1zLCBJbmMuMUgwRgYDVQQLDD8oYykgMjAxOSBSYWxseSBU\nYWN0aWNhbCBTeXN0ZW1zLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkx\nKzApBgNVBAMMIkVuZ2FnZSBGYWN0b3J5IERlZmF1bHQgQ2VydGlmaWNhdGUxIzAh\nBgkqhkiG9w0BCQEWFHN1cHBvcnRAcmFsbHl0YWMuY29tMIGbMBAGByqGSM49AgEG\nBSuBBAAjA4GGA
AQBFaIWniVXPmTjO4IcJmbXYCoKOSbqEjavLotDznTjEe0A+SJH\na4YLbIj5xTsfgq6RWRHpzFBuvOC2ToKbR6/92jIB0bHZlHL2vN6ADQi7qSC2RJEK\nJVIs1PLYfNfvNUhRvJJICPDJZ2D7xfXwxPpjLlZh+JhT6DBuGqR6QxtTRDNgpp4w\nCgYIKoZIzj0EAwIDgYwAMIGIAkIBCtpJbt
TuIRs7V4xOH8It78m1dv00gfHqOG0c\nMrUJHXJFLlT9yBTjiqK4NQsaYQgWiSrYpGLQ1I9h6A/D1yVId78CQgFa33PUA2nW\nTLabPBpdN3lno75IMHBHZ/nIPWW+KhZHzcS2pEXFT9X+KuKLpCZzos7/ZYr+tWJr\nlsr93Xdm0Z9nJA==\n-----END CERTIFICATE-----\n","channels":9600,"file":"good.wav","meta":"{\"engageEvent\":{\"alias\":\"MGR-ENG001\",\"audio\":{\"ms\":1200.0,\"samples\":9600},\"direction\":1,\"ended\":1665942587095,\"groupId\":\"eng001\",\"groupName\":\"Engineering\",\"id\":\"{8a1c4db0-8ac9-4b81-818f-7df3476a716c}\",\"nodeId\":\"{e4b4974d-c823-4584-933a-ff9c03024735}\",\"rxTxFlags\":0,\"rxTxPriority\":0,\"rxTxSubchannelTag\":0,\"started\":1665942585749,\"thisNodeId\":\"{8f48de49-a46a-4fc9-9aab-1a6b3bdf68fa}\",\"txId\":0,\"type\":1,\"uri\":\"file://./data/timelines/eng001/{8a1c4db0-8ac9-4b81-818f-7df3476a716c}.wav\"}}","sampleCount":0,"signature":"30818702411E55E0CAD1E1B2FC0EEC61D8E27547D558E67C233C97E9D5C5FC333AE4DFAF744B0AB130652B00B446E6789DD2CD5957AACBDD4479979FD42FD804E4181C8DD226024201AB4
05BBD093CAF1E84419B1AFCBB552F46673FBA7FD9C3FD4040238EAFA8481A075B707A83B74179725925AEE4E3FC21D4EA95596902344F466FC8CF56C1E2B67","verified":false}

Just gorgeous, isn't it. (And suuuuuper readable to boot!).

But if you have the jq tool available, you can pipe this output into jq and make it look a little prettier, as below:

$ ./riffverify good.wav --quiet --descriptor 2> /dev/null | jq .
{
  "certDescriptor": {
    "fingerprint": "7D:1A:6C:E6:C0:EC:53:1C:9D:99:20:48:81:E3:C3:87:43:31:C3:26",
    "issuer": "/C=US/ST=Washington/L=Seattle/O=Rally Tactical Systems, Inc./OU=(c) 2019 Rally Tactical Systems, Inc. - For authorized use only/CN=Rally Tactical Systems Root CA 1/[email protected]",
    "notAfter": "Sep  4 01:53:06 2029 GMT",
    "notBefore": "Sep  7 01:53:06 2019 GMT",
    "selfSigned": false,
    "serial": "AD:CB:61:C8:99:4E:21:E1",
    "subject": "/C=US/ST=Washington/L=Seattle/O=Rally Tactical Systems, Inc./OU=(c) 2019 Rally Tactical Systems, Inc. - For authorized use only/CN=Engage Factory Default Certificate/[email protected]",
    "version": 1
  },
  "certPem": "-----BEGIN CERTIFICATE-----\nMIIDZjCCAscCCQCty2HImU4h4TAKBggqhkjOPQQDAjCB9TELMAkGA1UEBhMCVVMx\nEzARBgNVBAgMCldhc2hpbmd0b24xEDAOBgNVBAcMB1NlYXR0bGUxJTAjBgNVBAoM\nHFJhbGx5IFRhY3RpY2FsIFN5c3RlbXMsIEluYy4xSDBGBgNVBAsMPyhjKSAyMDE5\nIFJhbGx5IFRhY3RpY2FsIFN5c3RlbXMsIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1\nc2Ugb25seTEpMCcGA1UEAwwgUmFsbHkgVGFjdGljYWwgU3lzdGVtcyBSb290IENB\nIDExIzAhBgkqhkiG9w0BCQEWFHN1cHBvcnRAcmFsbHl0YWMuY29tMB4XDTE5MDkw\nNzAxNTMwNloXDTI5MDkwNDAxNTMwNlowgfcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI\nDApXYXNoaW5ndG9uMRAwDgYDVQQHDAdTZWF0dGxlMSUwIwYDVQQKDBxSYWxseSBU\nYWN0aWNhbCBTeXN0ZW1zLCBJbmMuMUgwRgYDVQQLDD8oYykgMjAxOSBSYWxseSBU\nYWN0aWNhbCBTeXN0ZW1zLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkx\nKzApBgNVBAMMIkVuZ2FnZSBGYWN0b3J5IERlZmF1bHQgQ2VydGlmaWNhdGUxIzAh\nBgkqhkiG9w0BCQEWFHN1cHBvcnRAcmFsbHl0YWMuY29tMIGbMBAGByqGSM49AgEG\nBSuBBAAjA4GGAAQBFaIWniVXPmTjO4IcJmbXYCoKOSbqEjavLotDznTjEe0A+SJH\na4YLbIj5xTsfgq6RWRHpzFBuvOC2ToKbR6/92jIB0bHZlHL2vN6ADQi7qSC2RJEK\nJVIs1PLYfNfvNUhRvJJICPDJZ2D7xfXwxPpjLlZh+JhT6DBuGqR6QxtTRDNgpp4w\nCgYIKoZIzj0EAwIDgYwAMIGIAkIBCtpJbtTuIRs7V4xOH8It78m1dv00gfHqOG0c\nMrUJHXJFLlT9yBTjiqK4NQsaYQgWiSrYpGLQ1I9h6A/D1yVId78CQgFa33PUA2nW\nTLabPBpdN3lno75IMHBHZ/nIPWW+KhZHzcS2pEXFT9X+KuKLpCZzos7/ZYr+tWJr\nlsr93Xdm0Z9nJA==\n-----END CERTIFICATE-----\n",
  "channels": 9600,
  "file": "good.wav",
  "meta": "{\"engageEvent\":{\"alias\":\"MGR-ENG001\",\"audio\":{\"ms\":1200.0,\"samples\":9600},\"direction\":1,\"ended\":1665942587095,\"groupId\":\"eng001\",\"groupName\":\"Engineering\",\"id\":\"{8a1c4db0-8ac9-4b81-818f-7df3476a716c}\",\"nodeId\":\"{e4b4974d-c823-4584-933a-ff9c03024735}\",\"rxTxFlags\":0,\"rxTxPriority\":0,\"rxTxSubchannelTag\":0,\"started\":1665942585749,\"thisNodeId\":\"{8f48de49-a46a-4fc9-9aab-1a6b3bdf68fa}\",\"txId\":0,\"type\":1,\"uri\":\"file://./data/timelines/eng001/{8a1c4db0-8ac9-4b81-818f-7df3476a716c}.wav\"}}",
  "sampleCount": 0,
  "signature": "30818702411E55E0CAD1E1B2FC0EEC61D8E27547D558E67C233C97E9D5C5FC333AE4DFAF744B0AB130652B00B446E6789DD2CD5957AACBDD4479979FD42FD804E4181C8DD226024201AB4B05BBD093CAF1E84419B1AFCBB552F46673FBA7FD9C3FD4040238EAFA8481A075B707A83B74179725925AEE4E3FC21D4EA95596902344F466FC8CF56C1E2B67",
  "verified": false
}

OK, admittedly that's not all that beautiful. But that's mostly because your web browser or markdown reader isn't wide enough to accomodate those gigantic field values that come back. No worries, though. You can see it's JSON and there's fun to be had with it. Let's check out the fields in that JSON.

  • certDescriptor The public portion of X.509 certificate (parsed) used to sign the file (if signed).

  • certDescriptor.fingerprint The certificate's fingerprint as calculated by the underlying crypto module.

  • certDescriptor.issuer The certificate's issuing entity - the CA.

  • certDescriptor.notAfter The timestamp after which the certificate is no longer valid.

  • certDescriptor.notBefore The timestamp before which the certificate is not valid.

  • certDescriptor.selfSigned Indicates whether the certificate was self-signed.

  • certDescriptor.serial The certificate's serial number as calculated by the underlying crypto module.

  • certDescriptor.subject The certificate's subject data.

  • certDescriptor.version The certificate's X.509 version number.

  • certPem The public portion of X.509 certificate (PEM format) used to sign the file (if signed).

  • channels The number of audio channels in the file (if any).

  • file The full path to where the file was originally written by Engage.

  • meta A string of metadata associated with the file. It's usually JSON but could be some other string too. Present only if metadata was actually available and gathered by Engage when it made the recording.

  • sampleCount The number of audio samples in the file (if any).

  • signature The ECDSA signature in hex format (if signed).

  • verified True if the file passed verification, false if not.

Lots of this stuff is obviously useful in different ways but its worth pointing out the importance of that certificate field. Its important because all that Engage can tell you is that the RIFF was signed with that certificate. It cannot tell you whether the certificate is one you trust (maybe the file you're looking at is entirely spoofed by a bad guy?). So, to be sure to be sure (as Hollywood would have us believe everyone in Ireland says on a regular basis), you can use this certificate data to check the certificate according to your particular requirements.

You may, for example, want to verify that the certificate is one you trust by looking at who signed it (the Certificate Authority) or simply to see if it's one of yours by getting the signature of the certificate itself.

BONUS ITEM: For those sufficiently inquisitive, you may be wondering about the file name in the JSON vs the file name used in the examples above (good.wav and bad.wav). Might good.wav and bad.wav have a relationship with the file field in the JSON? .... You'd be right. Our two example files are just copies of the original (and then bad.wav was made bad by changing a byte it). Now, even though the name on the file system is, say, good.wav; if you look inside it, the meta data tells you that the file was originally written to file://./data/timelines/eng001/{8a1c4db0-8ac9-4b81-818f-7df3476a716c}.wav. Sweet!!

Going Full Geek

For those interested in exactly how the RIFF file is signed, here's the breakdown...

The RIFF consists of a number of chunks. When Engage is processing a RIFF for signing as part of the process of writing the file to storage, it creates an ECDSA signer object and writes those chunks as follows:

Chunk Included In Signature
RIFF Header (RIFF) No
Format Header (WAVE) Yes
Engage JSON Meta Data (json) Yes
RIFF Format (fmt ) Yes
Data (data) Yes
Engage X.509 Certificate (cert) Yes
Engage ECDSA Signature (sig ) No

When Engage processes a RIFF file for verification. It processes the chunks that were included in the signer - being WAVE, json, fmt , data, and cert - in that order.

We're telling you this so in the event you want to do perform independent verification, you can do so pretty easily. [Well, easy if you know how to work with certificates, ECDSA verifiers, keys, and other fun stuff. That is edge of (Aeron) chair stuff right here!]