Encoding data in RFC 4251 compliant format in Common Lisp

In a previous post I’ve discussed how to decode RFC 4251 binary data in Common Lisp. In this one we are going to see how to encode data in RFC 4251 compliant format.

Being able to encode data in RFC 4251 format would enable us to do some interesting things, e.g. generation of private/public key pairs and storing them on-disk in the proper format, just like you would do when using ssh-keygen(1). Another possibility would be to generate OpenSSH certificate keys and leasing these to clients when requested. You could also build something completely different and new, but actually base your protocol around the data types defined in RFC 4251.

Support for encoding data has already been merged in the cl-rfc4251 repo, and the test suite contains test cases for all data types defined in the RFC document.

The RFC4251:ENCODE generic function is the one that is responsible for encoding data into a binary stream. Also, there is now an implementation of a binary output stream, based on Gray streams, which can be created using the RFC4251:MAKE-BINARY-OUTPUT-STREAM.

Here are a few examples of encoding data using the cl-rfc4251 system. First, we need to load the system.

CL-USER> (ql:quickload :cl-rfc4251)

The following example shows how to encode an unsigned 32-bit integer.

CL-USER> (defparameter *s* (rfc4251:make-binary-output-stream))
CL-USER> (rfc4251:encode :uint32 42 *s*)
CL-USER> (rfc4251:binary-output-stream-data *s*)
#(0 0 0 42)

Note the result of the second expression above – the RFC4251:ENCODE generic function returns the number of bytes written to the stream.

Above example, can be written this way as well, if you only care about the actual encoded bytes.

CL-USER> (let ((s (rfc4251:make-binary-output-stream)))
           (rfc4251:encode :uint32 42 s)          ;; <- Encode the value into the stream
           (rfc4251:binary-output-stream-data s)) ;; <- Return the contents of the stream
#(0 0 0 42)

The following example encodes a string value.

CL-USER> (let ((s (rfc4251:make-binary-output-stream)))
           (rfc4251:encode :string "Hello, World!" s) ;; <- Encode the value into the stream
           (rfc4251:binary-output-stream-data s))     ;; <- Return the contents of the stream
#(0 0 0 13 72 101 108 108 111 44 32 87 111 114 108 100 33)

The following examples show how to encode mpint values according to RFC 4251.

CL-USER> (let ((s (rfc4251:make-binary-output-stream)))
           (rfc4251:encode :mpint #x00 s)         ;; <- Encode the zero value
           (rfc4251:binary-output-stream-data s)) ;; <- Get the encoded data
#(0 0 0 0)

Here are a few more examples taken directly from the examples section described in [RFC 4251][RFC 4251].

CL-USER> (let ((s (rfc4251:make-binary-output-stream)))
           (rfc4251:encode :mpint #x-DEADBEEF s)
           (rfc4251:binary-output-stream-data s))
#(0 0 0 5 255 33 82 65 17)
CL-USER> (let ((s (rfc4251:make-binary-output-stream)))
           (rfc4251:encode :mpint #x80 s)
           (rfc4251:binary-output-stream-data s))
#(0 0 0 2 0 128)
CL-USER> (let ((s (rfc4251:make-binary-output-stream)))
           (rfc4251:encode :mpint #x9A378F9B2E332A7 s)
           (rfc4251:binary-output-stream-data s))
#(0 0 0 8 9 163 120 249 178 227 50 167)
CL-USER> (let ((s (rfc4251:make-binary-output-stream)))
           (rfc4251:encode :mpint #x-1234 s)
           (rfc4251:binary-output-stream-data s))
#(0 0 0 2 237 204)

For additional examples, make sure to check out the included test suite.

Written on July 31, 2020