Recommendation: GSI SNEP (v1)
Structured header tag for signing message payloads with RSA/HMAC
The Global Scripting Institute (GSI) is an informal organization of Second Life® users that design and test standards for efficient, flexible, and readable scripts in Second Life. "Second Life®" and "Second Life Grid™" are trademarks of Linden Research, Inc., d/b/a Linden Lab. The Global Scripting Institute and its catalog are not affiliated with or sponsored by Linden Research.
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
The Signature/Nonce Extension Protocol (SNEP) is a signature format optimized for use with CLEP in SLua and Linden Scripting Language (LSL).
SLua-style syntax is used throughout this document, but all specifications also apply to the equivalent LSL functions and event callbacks.
SNEP is primarily useful in two situations:
SNEP is a signature only; it does not obfuscate the message to any degree and is not considered a form of encryption. Messages sent with SNEP are not private.
SNEP signatures are encoded JSON objects that contain the following key-value pairs:
| Key | Required? | Type | Value |
|---|---|---|---|
t |
REQUIRED | "RSA" or "HMAC" | If "RSA", the signature is an RSA signature. If "HMAC", the signature is an HMAC signature. |
h |
REQUIRED | "md5" (HMAC only), "sha1", "sha224", "sha256", "sha384", or "sha512" | MUST be the hash algorithm used when signing the message. |
k |
REQUIRED | string | MUST be a string uniquely identifying the key. This is used to allow the recipient to identify which key is used. It MAY be human-readable and SHOULD NOT be derived from the key itself. |
e |
REQUIRED | os.time() + seconds in LuallGetUnixTime() + seconds in LSL |
MUST be the Unix time when the signature expires. seconds MUST NOT exceed 300 seconds. |
n |
REQUIRED | uuid | MUST be ll.GenerateKey() or some other unique UUID used as a nonce. |
s |
REQUIRED | string | If sign_algo is "RSA", this MUST be the result of ll.SignRSA(private_key, tostring(e) .. payload, hash_algo).If sign_algo is "HMAC", this MUST be the result of ll.HMAC(shared_key, tostring(e) .. payload, hash_algo).e is the expiration time generated for the e key-value pair. |
All values MUST NOT contain newline (\n) characters.
The SNEP signature MUST be sent as a header named SNEP and the value MUST be the encoded JSON object. See the Examples section below for more information.
SNEP is only as secure as the underlying RSA and HMAC implementations. If RSA or HMAC is ever broken, SNEP will also be broken. Scripters SHOULD research known issues with RSA and HMAC before relying on them for security.
RSA and HMAC are vulnerable to replay attacks. These attacks can be mitigated in several ways:
e Check: Messages received with e less than the current Unix time SHOULD be discarded as stale. This check can be used in combination with a duplication check to reduce its long-term storage requirements. Make sure to validate that e is at most 300 seconds in advance; messages with longer lifespans SHOULD be dropped to avoid scripts intentionally filling up the hash storage with "immortal" messages.X-SecondLife-Object-Key to provide a guaranteed truthful UUID for this check if validating in an external server.A duplication check and an e check is the RECOMMENDED method to mitigate replay attacks.
Scripters concerned about LSL support - either in their own scripts, or for compatibility with others' LSL scripts - should note the following caveats:
e parameter is subject to the Year 2038 problem due to LSL's 32-bit architecture. After 2038, LSL scripts will only be able to verify signatures from other LSL scripts, and messages signed by LSL scripts will be unverifiable by other languages (including SLua) without mitigations. It is, therefore, RECOMMENDED to avoid relying on SNEP in LSL except for transitional scripts.-- Channel Hash Utility Function https://gsi.sh/rec/chuf
local domain = "Domain with objects that can be reset remotely"
local channel = bit32.bor(ll.Hash(domain), 0xC0000000)
if channel >= 2^31 then channel -= 2^32 end
local payload = lljson.slencode({
method = {"Command", "Reset"},
domain = domain,
target = {},
source = { -- these values MUST be included, even for link messages
region = ll.GetRegionName(),
prim = ll.GetKey(),
script = ll.GetScriptName()
},
id = ll.GenerateKey() -- MUST be included
})
local hmac_key_name = "HMAC key name goes here"
local hmac_secret = "HMAC secret goes here"
local hash_algo = "sha256"
local expire_time = os.time() + 60
local snep_signature = {
t = "HMAC",
h = hash_algo,
k = hmac_key_name,
e = expire_time,
s = ll.HMAC(hmac_secret, tostring(expire_time) .. payload, hash_algo)
}
ll.RegionSay(
channel,
`SNEP:{snep_signature}\n` ..
payload
)
Which signature algorithm should I use, RSA or HMAC?
RSA is asymmetric; the keys used by the sender and recipient are not the same, and the sender's private key cannot be derived from knowing the recipient's public key. RSA provides an arguably higher level of practical security when handling SL scripts, because if the recipient's public key is disclosed, the sender's ability to sign messages with the private key is not compromised.
HMAC is symmetric; the same key must be used when signing and verifying. Its benefit is that its signatures are smaller (24-88 characters, depending on the hash algorithm, compared to 172-684 characters for RSA, for 1024-4096 bit keys) and it takes less time to generate and verify signatures (approximately 1 frame or ~0.022s, compared to approximately 3 frames or ~0.067s for RSA in testing). The downside is that if the shared key is exposed on either end, signatures may be forged. Therefore, HMAC SHOULD NOT be used in modifiable scripts or stored anywhere that could be exposed to a third party.
Which hash algorithm should I use?
"md5" and "sha1" are considered insecure and SHOULD NOT be used. Other SHA algorithms are considered safe, but larger ones confer better security against collisions. There does not appear to be any significant difference in performance when testing different hash algorithms.
ll.SignRSA() supports "sha1", "sha224", "sha256", "sha384", or "sha512". There is no difference in processing time or the size of the resulting signature, so scripts SHOULD use "sha256" or "sha512" when signing with RSA.
ll.HMAC() supports all the same SHA algorithms, plus "md5". Each algorithm results in a different signature size:
hash_algo |
Base64 Characters |
|---|---|
| "md5" | 24 (trivially broken) |
| "sha1" | 28 (broken) |
| "sha224" | 40 |
| "sha256" | 44 |
| "sha384" | 64 |
| "sha512" | 88 |
"sha256" or "sha512" is RECOMMENDED for HMAC, but if the shortest possible signature is necessary, a smaller hash MAY be used.
What key length should I use for my RSA keys?
Testing shows that 1024 and 2048 bit keys are typically fast, signing and verifying in a few frames at most. 4096 bit keys take significantly longer to sign - about a quarter of a second. Signature sizes also change depending on the key length:
| Key Length | Base64 Characters |
|---|---|
| 1024 | 172 |
| 2048 | 344 |
| 4096 | 684 |
Key lengths of at least 2048 are currently RECOMMENDED, but 1024 is acceptable for limited-memory LSL scripts. Key lengths below 1024 bits are generally considered insecure and SHOULD NOT be used.