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 theRIFF
,fmt
, anddata
chunks in the file as that's the bare minimum they need to play audio. Engage adds more chunks namedjson
,cert
, andsig
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
andbad.wav
). Mightgood.wav
andbad.wav
have a relationship with thefile
field in the JSON? .... You'd be right. Our two example files are just copies of the original (and thenbad.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 tofile://./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!]