Liking cljdoc? Tell your friends :D

buddy-sign - High level message signing.

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 "3.1.0"]

And is tested under 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 nameHash algorithmsKeywordsPriv/Pub Key?

Elliptic Curve DSA

sha256, sha512

:es256, :es512

Yes

Edwards Curve DSA

sha512

:eddsa

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 nameDecriptionKeywordShared 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 nameDescriptionKeywordShared 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.

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 unsign 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 demonstration 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 nameHash algorithmsKeywordsPriv/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"

Can you improve this documentation? These fine people already did:
Andrey Antukh, Earl St Sauver, Mikhail Gusarov, Denis Shilov, Josh Graham, Edwin Shin, Jamie, J. Pablo Fernández, Blake Grotewold & Ducky
Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close