Introduction

Buddy sign module is dedicated to provide a high level abstraction for web ready message signing and encryption.

It can be used for several purposes:

  • You can serialize and sign or encrypt a user ID for unsubscribing of newsletters into URLs. This way you don’t need to generate one-time tokens and store them in the database.

  • Same thing with any kind of activation link for accounts and similar things.

  • Signed or encrypted objects can be stored in cookies or other untrusted sources which means you don’t need to have sessions stored on the server, which reduces the number of necessary database queries.

  • Signed information can safely do a roundtrip between server and client in general which makes them useful for passing server-side state to a client and then back.

  • Safely send and receve signed or encrypted messages between components or microservices.

  • Self contained token generation for use with completely stateless token based authentication.

Project Maturity

Since buddy-sign is a young project there can be some API breakage.

Install

The simplest way to use buddy-sign in a clojure project, is by including it in the dependency vector on your project.clj file:

[buddy/buddy-sign "2.2.0"]

And is tested under JDK7 and JDK8.

Json Web Token

JSON Web Token (JWT) is a compact claims representation format intended for space constrained environments such as HTTP Authorization headers and URI query parameters. JWTs encode claims to be transmitted as a JavaScript Object Notation (JSON) object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or MACed and/or encrypted.

Supported algorithms

For claims signing

Here a table of supported algorithms for signing JWT claims using JWS (Json Web Signature):

Algorithm name Hash algorithms Keywords Priv/Pub Key?

Elliptic Curve DSA

sha256, sha512

:es256, :es512

Yes

RSASSA PSS

sha256, sha512

:ps256, :ps512

Yes

RSASSA PKCS1 v1_5

sha256, sha512

:rs256, :rs512

Yes

HMAC

sha256*, sha512

:hs256, :hs512

No

For claims encryption

The Json Web Encryption in difference to JWS uses two types of algoritms: key encryption algorithms and content encryption algorithms.

The key encryption algorithms are responsible of encrypt the key that will be used for encrypt the content. This is a table that exposes the currently supported Key Encryption Algorithms (specified in JWA RFC):

Algorithm name Decription Keyword Shared Key Size

DIR

Direct use of a shared symmetric key

:dir

(depends on content encryption algorithm)

A128KW

AES128 Key Wrap

:a128kw

16 bytes

A192KW

AES192 Key Wrap

:a192kw

24 bytes

A256KW

AES256 Key Wrap

:a256kw

32 bytes

RSA1_5

RSA PKCS1 V1_5

:rsa1_5

Asymetric key pair

RSA-OAEP

RSA OAEP with SHA1

:rsa-oaep

Asymetric key pair

RSA-OAEP-256

RSA OAEP with SHA256

:rsa-oaep-256

Asymetric key pair

The content encryption algoritms are responsible of encrypt the content. This is a table that exposes the currently supported Content Encryption Algorithms (all specified in the JWA RFC):

Algorithm name Description Keyword Shared Key Size

A128CBC-HS256

AES128 with CBC mode and HMAC-SHA256

:a128cbc-hs256

32 bytes

A192CBC-HS384

AES192 with CBC mode and HMAC-SHA384

:a192cbc-hs384

48 bytes

A256CBC-HS512

AES256 with CBC mode and HMAC-SHA512

:a256cbc-hs512

64 bytes

A128GCM

AES128 with GCM mode

:a128gcm

16 bytes

A192GCM

AES192 with GCM mode

:a192gcm

24 bytes

A256GCM

AES256 with GCM mode

:a256gcm

32 bytes

Signing data

Let start with signing data. For it we will use the sign function from buddy.sign.jws namespace, and the hs256 algorithm for signining:

(require '[buddy.sign.jwt :as jwt])

(jwt/sign {:userid 1} "secret")
;; "eyJ0eXAiOiJKV1MiLCJhbGciOiJIU..."

The sign function return a encoded and signed token as plain String instance or an exception in case of something goes wrong. As you can observe, no algorithm is passed as parameter. In this situations the default one will be used, and in this case is :hs256.

Note
Due to the nature of the storage format, the input is restricted mainly to json objects in the current version.

Unsigning data

It’s time to unsing data. That process consists on verify the signature of incoming data and return the plain data (without signature). For it we will use the unsign function from buddy.sign.jwt namespace:

(jwt/unsign data "secret")
;; => {:userid 1}

Claims validation

buddy-sign json web signature implements validation of a concrete subset of claims: iat (issue time), exp (expiration time), nbf (not before), iss (issuer) and aud (audience).

The validation is performed on decoding the token. If :exp claim is found and is posterior to the current date time (UTC) an validation exception will be raised. Alternatively, the time to validate token against can be specified as :now option to unsign.

Additionaly, if you want to provide some leeway for the claims validation, you can pass the :leeway option to the unsign function.

Let see an example using direct api:

(require '[clj-time.core :as time])

;; Define claims with `:exp` key
(def claims
  {:user 1 :exp (time/plus (time/now) (time/seconds 5))})

;; Serialize and sign a token with previously defined claims
(def token (jwt/sign claims "key"))

;; wait 5 seconds and try unsign it

(jwt/unsign token "key")
;; => ExceptionInfo "Token is older than :exp (1427836475)"

;; use timestamp in the past
(jwt/unsign token "key" {:now (time/minus (time/now) (time/seconds 5))})
;; => {:user 1}

Encrypting data

Let start with encrypting data. For it we will use the encrypt function from the buddy.sign.jwt namespace:

(require '[buddy.sign.jwt :as jwt])
(require '[buddy.core.hash :as hash])

;; Hash your secret key with sha256 for
;; create a byte array of 32 bytes because
;; is a requirement for default content
;; encryption algorithm

(def secret (hash/sha256 "mysecret"))

;; Encrypt it using the previously
;; hashed key

(jwt/encrypt {:userid 1} secret {:alg :dir :enc :a128cbc-hs256})
;; "eyJ0eXAiOiJKV1MiLCJhbGciOiJIU..."

The encrypt function, like sign from JWS, returns a plain string with encrypted and encoded content using a provided algorithm and shared secret key.

Decrypting Data

The decrypt is a inverse process, that takes encrypted data and the shared key, and returns the plain data. For it, buddy-sign exposes the decrypt function. Let see how you can use it:

(jwt/decrypt incoming-data secret)
;; => {:userid 1}

Digital signature algorithms

In order to use any of digital signature algorithms you must have a private/public key. If you don’t have one, don’t worry, it is very easy to generate it using openssl, see this faq entry.

Now, having generated a key pair, you can sign your messages using one of supported digital signature algorithms.

Example of signing a string using es256 (eliptic curve dsa) algorithm.
(require '[buddy.core.keys :as keys])

;; Create keys instances
(def ec-privkey (keys/private-key "ecprivkey.pem"))
(def ec-pubkey (keys/public-key "ecpubkey.pem"))

;; Use them like plain secret password with hmac algorithms for sign
(def signed-data (jwt/sign {:foo "bar"} ec-privkey {:alg :es256}))

;; And unsign
(def unsigned-data (jwt/unsign signed-data ec-pubkey {:alg :es256}))

Asymetric encryption schemes

In order to use any asymetric encryption algorithm, you should have private/public key pair. If you don’t have one, don’t worry, it is very easy to generate it using openssl, see this faq entry.

Then, having ready the key pair, you can strart using one of the supported key encryption algorithm in the JWE specification such as :rsa1_5, :rsa-oaep or :rsa-oaep-256.

Let see an demostration example:

(require '[buddy.core.keys :as keys])

;; Create keys instances
(def privkey (keys/private-key "privkey.pem"))
(def pubkey (keys/public-key "pubkey.pem"))

;; Encrypt data
(def encrypted-data (jwt/encrypt {:foo "bar"} pubkey
                                 {:alg :rsa-oaep
                                  :enc :a128cbc-hs256})

;; Decrypted
(def decrypted-data (jwt/decrypt encrypted-data privkey
                                 {:alg :rsa-oaep
                                  :enc :a128cbc-hs256}))

Json Web Signature

JSON Web Signature (JWS) is a signing part of Json Web Token (JWT) specification and represents a content secured with digital signatures or Message Authentication Codes (MACs) using JavaScript Object Notation (JSON) as serialization format.

In difference to JWT, this is more lowlevel signing primitive and allows signining arbitrary data (instead of json formated claims):

(require '[buddy.sign.jws :as jws])
(require '[buddy.core.nonce :as nonce])
(require '[buddy.core.bytes :as bytes])

(def data (nonce/random-bytes 1024))
(def message (jws/sign data "secret"))

(bytes/equals? (jws/unsign message "secret") data)
;; => true

Json Web Encryption

JSON Web Encryption (JWE) is a encryption part of Json Web Token (JWT) specification and represents a encrypted content using JavaScript Object Notation (JSON) based data structures.

In same way as JWS, this is a low level primitive that allows create fully encrypted messages of arbitrary data:

(require '[buddy.sign.jws :as jws])
(require '[buddy.core.nonce :as nonce])
(require '[buddy.core.bytes :as bytes])

(def key32 (nonce/random-bytes 32))
(def data (nonce/random-bytes 1024))

(def message (jwt/encrypt data key32))
(bytes/equals? (jws/decrypt message key32) data)
;; => true

Compact message signing

Compact high level message signing implementation.

It has high influence by django’s cryptographic library and json web signature/encryption but with focus on have a compact representation. It’s build on top of fantastic ptaoussanis/nippy serialization library.

In order to use this you shall include the concrete nippy library because buddy-sign does not have a hardcoded dependency to it:

[com.taoensso/nippy "2.11.1"]

In the same way as JWS, it support a great number of different signing algorithms that can be used for sign your messages:

Algorithm name Hash algorithms Keywords Priv/Pub Key?

Elliptic Curve DSA

sha256, sha512

:es256, :es512

Yes

RSASSA PSS

sha256, sha512

:ps256, :ps512

Yes

RSASSA PKCS1 v1_5

sha256, sha512

:rs256, :rs512

Yes

Poly1305

aes, twofish, serpent

:poly1305-aes, :poly1305-serpent, :poly1305-twofish

No

HMAC

sha256*, sha512

:hs256, :hs512

No

* indicates the default value.

In difference with jwt/jws, this implementation is not limited to hash-map like objects, and you can sign any clojure valid type.

Let see an example:

(require '[buddy.sign.compact :as cm])

(def data (cp/sign #{:foo :bar} "secret")

(cm/unsign data "secret")
;; => #{:foo :bar}

Then, you also will be able validate the signed message based in its age:

(cm/unsign data "secret" {:max-age (* 15 60)})
;; => ExceptionInfo: "Token is older than 1427836475"

FAQ

When I should use JWE and when JWS?

The main difference between JWS and JWE, is that JWE encrypts the claims with an algorithm that uses a one time key. Both provides good security, but JWE also provides privacy of the data.

If you only stores the userid or something similar, JWS is recommended, because it has less overhead. But if you are storing in the token claims that require privacy, JWE is the solution that should be used.

How I can generate keypairs?

Example on how to generate one Elliptic Curve DSA keypair.
# Generating params file
openssl ecparam -name prime256v1 -out ecparams.pem

# Generate a private key from params file
openssl ecparam -in ecparams.pem -genkey -noout -out ecprivkey.pem

# Generate a public key from private key
openssl ec -in ecprivkey.pem -pubout -out ecpubkey.pem
Example on how to generate one RSA keypair.
# Generate aes256 encrypted private key
openssl genrsa -aes256 -out privkey.pem 2048

# Generate public key from previously created private key.
openssl rsa -pubout -in privkey.pem -out pubkey.pem

Developers Guide

Contributing

Unlike Clojure and other Clojure contributed libraries buddy-sign does not have many restrictions for contributions. Just open an issue or pull request.

Philosophy

Five most important rules:

  • Beautiful is better than ugly.

  • Explicit is better than implicit.

  • Simple is better than complex.

  • Complex is better than complicated.

  • Readability counts.

All contributions to buddy-sign should keep these important rules in mind.

Source Code

buddy-sign is open source and can be found on github.

You can clone the public repository with this command:

git clone https://github.com/funcool/buddy-sign

Run tests

For running tests just execute this:

lein test-all

License

buddy-sign is licensed under Apache 2.0 License. You can see the complete text of the license on the root of the repository on LICENSE file.