by Jean Zee (a.k.a. CmpZ)
created Monday, 2021 September 20
updated Tuesday, 2021 November 2
There are ad hoc schemes for web servers to determine that messages they receive originate from intended objects within Second Life.[00]
When it comes to authentication & validation, ad hoc is dangerous, so here's a technique for authentication between LSL scripts & external web servers. It builds on a trustworthy standard called HMAC, but I've modified it to make it easier to use in LSL.
TL, DR: Jump to the All the code section.
It doesn't take much effort to extend the use case to allow the LSL script to verify the reply it receives from the web server. I'll leave that to you.
We'll adapt the HMAC algorithm for use with LSL.[02]
HMAC is defined in bit-twiddling terms, but LSL doesn't do bits all that well. It's better with strings. And the llSHA1String function that the Lindens give us operates on strings of text, even includes warnings about arbitrary binary values. So we need to adapt the HMAC algorithm into something appropriate for LSL. (See “Adapting HMAC for LSL”.) I called the result FakeMAC.
Here's how you use FakeMAC:
Let's say that your LSL script sends an HTTP request to your web server, sort of like this...
string Create_Request_Body() { return some string; } string THE_URL = "http://example.com/whatever/goop/goes/here"; // *** // This is your original Send_To_Server function // ** Send_To_Server() { list parameters; string body; parameters = [ HTTP_METHOD, "POST" ]; body = Create_Message_Body(); llHTTPRequest( THE_URL, parameters, body ); }
To use FakeMAC, you first create a secret that's shared between your script & your web server...
// The web server knows this same secret. string SHARED_SECRET = "j$rA+KiB7Rzad7^&=G*Yo_QN";
You copy the Fake_MAC
function from
fakemac.lsl
into your script.[03]
string Fake_MAC( string secret, string message ) { string o_pad; string i_pad; o_pad = llSHA1String( secret + "ooo" ); i_pad = llSHA1String( secret + "iii" ); return llSHA1String( o_pad + llSHA1String( i_pad + message ) ); }
Modify your Send_To_Server
function so that, after
constructing the main body, it calls Fake_MAC
to
obtain a Message Authentication Code (MAC). And append that MAC
to the body.
// *** // This is your NEW Send_To_Server function // ** Send_To_Server() { list parameters; string real_message; string b64; string body; parameters = [ HTTP_METHOD, "POST" ]; real_message = Create_Message_Body(); b64 = llStringToBase64( real_message ); body += "\n" + Fake_MAC( SHARED_SECRET, b64 ); llHTTPRequest( THE_URL, parameters, body ); }
(Q. Why call llStringToBase64
?
A. See the Protips section for the answer.)
And now you need to make changes on your server.
Your server receives the message. Before FakeMAC, your server just received the message (the only part), did some ad hoc verification, then did the real work.
When using FakeMAC, your server receives both the message & the FakeMAC code. Here's what the server does now (pseudocode)...
= Fake_MAC( SHARED_SECRET, real message)
.received code
,
reject the message because there's been tampering or forgery.Notice that when your server has a reply ready, it can package that with a FakeMAC code so that your LSL script can authenticate it.
I created several prototype implementations. Pick your favorite language.
Inside Second Life, you'll of course need fakemac.lsl.
Those are stand-alone, proof-of-concept programs. You'll need
to copy the Fake_MAC
function from one of them into
your server, then do some work to call it.
This turns out to be an interesting question, not always a simple answer. I'll write about it in another file (exn6p.html).
In my example, I've packaged the real messge with the code as two lines. I did that because it's easy for me to parse on my server.
When you package them together, pick a format that's easy to parse on your own server.
These days, JSON is often a good choice. (Maybe you already package your real message in JSON.) If your real message is already a JSON list or object, I caution against simply adding the code as a field to that! The reason is that the SHA1 hash (or any other hash algorithm we use) is sensitive to different amounts of space characters, tabs, & end-of-line sequences. (Remember that end-of-line sequences sometimes, annoyingly, differ from host to host or when sent over the wire.) If you simply add the code as an element to your existing real message, then you'll need to remove that code in a way that exactly (octet-for-octet) recovers the message on the sender's side before you calculate the actual code. It can be done, but in my experience, it's more frustrating than packaging an unaltered real message into an outer message that contains it & the code.
That leads to protip 2! (Keep reading.)
The SHA1 algorithm is sensitive to any change in the values it
hashes. For example, “hello
” followed
by an ASCII line feed (#x0A) gets a different value than
“hello
” followed by a carriate return +
line feed (#x0D #x0A). Check it out:
hello
” + #x0A) = f572d396fae9206628714fb2ce00f72e94f2258f
hello
” + #x0D #x0A) = aa5916ae7fd159a18b1b72ea905c757207e26689
In addition, the llSHA1String
function's documentation warns of
“zero-byte value into this function, nor any byte value from 128-255”.[See “Caveats” in 01]
To protect against bugs caused by these realities, I recommend Base64-encoding your real message, then calculating the FakeMAC for that. Combine those into an outer message & send it. So you are sending a base64-encoded real message plus the code (which is in base 16, so also safe).
When you are figuring out why your server calculates a different code than does your LSL script, remember to print out every octet in the message before you send it & after you receive it, even the end-of-lines.
Include a timestamp in your real message. Your receiver can use it to verify that the message isn't too old (or isn't too far in the future). This protects against a monster-in-the-middle (MitM) that delayed the message for a long time.
The timestamp must be inside your real message that you then give to FakeMAC. You can't rely on the Date in the HTTP headers because the MitM could hold the entire message for a while, then forward it but with a new Date header.
It's also a little safer to include a nonce. This ensures that every message has a unique real message even if you send the same values so quickly that the timestamp doesn't change. The nonce can be as simple as a value you get from llFrand.
HMAC is an acronym for Hashed Message Authentication Code. It allows two entities to authenticate each other's messages when they share a secret.[02]
Here's how I derived FakeMAC for LSL from HMAC.
llSHA1String
function operates on a string & gives
us a string. So I created an algorithm that operates on strings.For the inner padded key and outer padded key in FakeMAC, I
appended iii
and ooo
. Those values
aren't critical. You could replace them with other
sequences.
HMAC was created by cryptographic experts, which I'm not. I would not be surprised if, by creating FakeMAC, I've reduced the trust that can be placed in HMAC.
On the other hand, I suspect that very few people can tamper with a message in a way that FakeMAC can't detect. Cryptographic experts created HMAC, & hopefully a cryptographic expert is required to fool FakeMAC.
And FakeMAC is a lot better than a reverse DNS lookup, a custom HTTP header, inserting a secret in the message, & most other ad hoc attempts.
The SHA1 cryptographic hash function is old, replaced by SHA2 in 2001 (long time ago), & that has been replaced by SHA3. So SHA1 is soooo yesterday!
LSL gives us a llSHA1String
function, no others.
When LSL offers a llSHA2String
or
llSHA3String
function, we just update this
Fake_MAC
function to use it. (And then update
your server code.)
I am aware of the user-supplied, {free, libre, open} source
SHA2 implementation that's available
on wiki.secondlife.com. I gave it
a try, but after a couple of hours of debugging, it was still
producing hash values that differ from what I could get on my
server. I concluded I had spent enough time on it &
switched to LSL's llSHA1String
.
FakeMAC sounds a sort of like HMAC, but it's not HMAC. That's sort of like how the FakeMAC algorithm is sort of the HMAC algorithm.
I could have called it LSLMAC, but that'd be pretentious; I don't speak for LSL. More importantly, it creates an upcase/downcase problem. Would it be LSLMAC or LslMAC? (There are some questions that mortals should never ask.)
The Second Life forums might be best: 477996-....
Or contact me in Second Life.
My display name is “Jean Zee”, & my username is
CmpZ
.
Indeed, upcase is already used for constants. I'm trying them out for globals. It's an experiment.
I also notice that, in LSL at least, constants are globals.
llSHA1String
when | who | what |
---|---|---|
2021-09-24 | CmpZ | The “string key” parameter of the
Fake_MAC function in LSL caused an error
because key is a type in LSL. So I changed
the parameter's name to secret . |
2021-10-06 | CmpZ | Minor edits to improve readability. This is a publishable version. Linked to 2L forum. |
2021-11-02 | CmpZ | Edits for spelling & readability |