Wednesday, February 22, 2012

Capturing Envoy Data

Pursuant to my last post, I've written a simple man-in-the-middle proxy to intercept communication between the Envoy and the Enphase servers. The code is available here.

What it does


As I detailed in my previous post, the Envoy sends data to Enphase via http POST requests. The proxy intercepts these requests, extracts the XML data from the request, and writes it to a local file (by default in /var/spool/envoy). It then forwards the request on to Enphase, and returns the reply to your Envoy.

In addition to extracting the XML data, the proxy also logs the complete contents (headers and message content) of the request and the reply to files.

How it works


Out of the box, your Envoy configures itself automatically using DHCP. Possibly, you've configured it statically. In either case, it will typically be configured to connect via your default gateway -- generally, your home router, cable modem, etc. In order to intercept the communication between the Envoy and Enphase, we insert another server between the Envoy and your network gateway. In the foollowing diagram, the dotted line represents the original communication path, while the solid lines represent the new communication path:


The intermediate system -- which we'll call the interceptor -- will use a few tricks to redirect traffic destined for Enphase to the local proxy (which will log the data locally and then forward it on to Enphase).

Assumptions


For the purposes of this article, we'll assume that your Envoy is at address 192.168.1.100, and the address of the interceptor is 192.168.1.200.

I'm assuming that your interceptor is running Linux. It may be possible to accomplish the same thing with other tools, but I'm relying on the Linux netfilter subsystem (aka "iptables") to perform certain key tasks.

Configuring the Envoy


You will need to congfigure the Envoy to use your interceptor host as its default gateway.

  1. Go to the network connectivity page on your Envoy.
  2. If it's checked, uncheck the "Use DHCP" setting and select the "Updating DHCP setting" button.
  3. Set the "Gateway IP" field to the address of your interceptor (192.168.1.200 in this example).
  4. Select the "Update Interface 0" button.

Configuring the interceptor


Redirecting requests


We need to configure the interceptor to redirect requests to the Enphase servers to a local application. We'll add the following firewall rule:
iptables -t nat -A PREROUTING -s 192.168.1.100 -p tcp \
  --dport 443 -j REDIRECT --to-ports 4430
This rule matches https (port 443) requests from your Envoy (192.168.1.100) and redirects them to port 4430 on the interceptor.
Note that this rule will be lost if you reboot your system. Making firewall rules persistent is beyond the scope of this article; consult the documentation for your distribution of choice.

Handling SSL


My simple Python proxy doesn't speak SSL, so we need to create a plain http request from the https request. Normally this would be difficult, but Enphase has made our life easier by not checking the validity of the SSL certificate. We're going to use stunnel as an https-to-http proxy. Create a file called /etc/stunnel/envoy-ssl.conf with the following contents:
[https_in]
accept = 4430
cert = /etc/pki/tls/certs/localhost.crt
connect = 127.0.0.1:8080
Run stunnel with this configuration:
stunnel /etc/stunnel/envoy-ssl.conf

This assumes you have an SSL certificate in /etc/pki/tls/certs/localhost.crt. You will probably need to generate one, which again is left as an exercise to the reader.

Installing bottle


The proxy relies on the bottle Python web framework, which is probably not installed on your system. The easiest way to get things going is to install a Python "virtual environment" with the appropriate modules. Create a new virtual environment:

virtualenv ~/env/envoy

And install bottle:

~/env/envoy/bin/pip install bottle

Creating directories


By default the proxy will write data to /var/spool/envoy. You'll need to make sure this directory exists and is writable by whatever account you're using to run the proxy.


Running the proxy


Now that you've got all the prerequisites in place, you should be able to start the proxy by running:

~/env/envoy/bin/python proxy.py

You should see something like this:

Bottle server starting up (using WSGIRefServer())...
Listening on http://127.0.0.1:8080/
Hit Ctrl-C to quit.

Assuming that everything else went as planned, sometime within the next five minutes you should see the proxy service a request from your Envoy:


localhost.localdomain - - [22/Feb/2012 09:03:57] "POST /emu_reports/
performance_report?webcomm_version=3.0 HTTP/1.1" 200 103

From this request you will end up with three files in /var/spool/envoy:

  • 2012-02-22T09:03:01-0j1FFs.xml
    This is the XML data from the Envoy and is probably the most interesting file.

  • 2012-02-22T09:03:01-ZMxw6b.request
    This is the raw request from the Envoy.

  • 2012-02-22T09:03:02-NB4DbR.response
    This is the response from the Enphase servers.


If you find some bugs, please let me know by creating a new issue here. Note that this is only for bugs in the code; if you need basic networking tutorials and so forth the Google has lots of help for you.


Monday, February 13, 2012

Enphase Envoy XML Data Format

We recently installed a (photovoltaic) solar array on our house.  The system uses Enphase microinverters, and includes a monitoring device called the "Envoy".  The Envoy collects data from the microinverters and sends it back to Enphase.  Enphase performs monitoring services for the array and also provides access to the data collected by the Envoy product.

I'm interested in getting direct access to the data provided by the Envoy.  In pursuit of that goal, I set up a man-in-the-middle proxy server on my home network to intercept the communication from the Envoy to the Enphase servers.  I'm documenting the results of my exploration here in case somebody else finds the information useful.

The Envoy sends deflate-encoded XML data to the Enphase servers. You can decompress the data using Python's zlib module like this:

import zlib
xml_data = zlib.decompress(deflate_compressed_data)

The request is an http POST to https://reports.enphaseenergy.com/emu_reports/performance_report?webcomm_version=3.0. The request headers look like this:

POST /emu_reports/performance_report?webcomm_version=3.0 HTTP/1.1
Accept: */*
Connection: close
Content-Type: application/x-deflate
Content-Length: 729
Host: reports.enphaseenergy.com:443

The request body -- after inflating it -- looks something like this:

<?xml version='1.0' encoding='utf-8'?>
<perf_report report_timestamp='1329134202'>
  <envoy ip_addr='192.168.1.166' mac_addr='00:11:22:33:44:55'
  timezone='US/Eastern' part_num='800-00024-r04'
  sw_version='R3.0.0 (9720d7)' serial_num='123456115374' />
  <event correlation_id='161' event_state='1' id='221'
  event_code='10009' eqid='123456102635' serial_num='123456102635'
  event_date='1329134118' />
  <event correlation_id='194' event_state='1' id='222'
  event_code='101' eqid='123456103685.1' serial_num='123456103685'
  event_date='1329134119' />
  <event correlation_id='196' event_state='1' id='223'
  event_code='101' eqid='123456105331.1' serial_num='123456105331'
  event_date='1329134120' />
  <event correlation_id='202' event_state='1' id='224'
  event_code='101' eqid='123456103294.1' serial_num='123456103294'
  event_date='1329134130' />
  <event correlation_id='206' event_state='1' id='225'
  event_code='101' eqid='123456105321.1' serial_num='123456105321'
  event_date='1329134139' />
  <event event_state='2' id='226' event_code='101'
  eqid='123456104335.1' serial_num='123456104335'
  event_date='1329134152' />
  <event event_state='0' id='227' event_code='509'
  eqid='123456104335' serial_num='123456104335'
  event_date='1329134152' />
  <event correlation_id='213' event_state='1' id='228'
  event_code='101' eqid='123456105296.1' serial_num='123456105296'
  event_date='1329134153' />
  <device image_bits='131072' device_type='1' admin_state='1'
  condition_flags='2359296' part_num='800-00103-r05'
  observed_flags='0' eqid='123456105041' control_bits='0'>
    <channel channel_type='1' condition_flags='16'
    observed_flags='0' eqid='123456105041.1' control_bits='0' />
  </device>
  <device image_bits='131072' device_type='1' admin_state='1'
  condition_flags='2359296' part_num='800-00103-r05'
  observed_flags='0' eqid='123456104335' control_bits='0'>
    <channel channel_type='1' condition_flags='16'
    observed_flags='0' eqid='123456104335.1' control_bits='0' />
  </device>
  <device image_bits='131072' device_type='1' admin_state='1'
  condition_flags='2359296' part_num='800-00103-r05'
  observed_flags='0' eqid='123456104246' control_bits='0'>
    <channel channel_type='1' condition_flags='16'
    observed_flags='0' eqid='123456104246.1' control_bits='0' />
  </device>
  <device image_bits='131072' device_type='1' admin_state='1'
  condition_flags='2359296' part_num='800-00103-r05'
  observed_flags='0' eqid='123456104224' control_bits='0'>
    <channel channel_type='1' condition_flags='16'
    observed_flags='0' eqid='123456104224.1' control_bits='0' />
  </device>
  <device image_bits='131072' device_type='1' admin_state='1'
  condition_flags='2359296' part_num='800-00103-r05'
  observed_flags='0' eqid='123456102776' control_bits='0'>
    <channel channel_type='1' condition_flags='16'
    observed_flags='0' eqid='123456102776.1' control_bits='0' />
  </device>
  <device image_bits='131072' device_type='1' admin_state='1'
  condition_flags='2359296' part_num='800-00103-r05'
  observed_flags='0' eqid='123456103271' control_bits='0'>
    <channel channel_type='1' condition_flags='16'
    observed_flags='0' eqid='123456103271.1' control_bits='0' />
  </device>
  <device image_bits='131072' device_type='1' admin_state='1'
  condition_flags='2359296' part_num='800-00103-r05'
  observed_flags='0' eqid='123456105190' control_bits='0'>
    <channel channel_type='1' condition_flags='16'
    observed_flags='0' eqid='123456105190.1' control_bits='0' />
  </device>
  <device image_bits='131072' device_type='1' admin_state='1'
  condition_flags='2097152' part_num='800-00103-r05'
  observed_flags='0' eqid='123456105321' control_bits='0'>
    <channel channel_type='1' condition_flags='0'
    observed_flags='0' eqid='123456105321.1' control_bits='0' />
  </device>
  <device image_bits='131072' device_type='1' admin_state='1'
  condition_flags='2359296' part_num='800-00103-r05'
  observed_flags='0' eqid='123456105178' control_bits='0'>
    <channel channel_type='1' condition_flags='16'
    observed_flags='0' eqid='123456105178.1' control_bits='0' />
  </device>
  <device image_bits='131072' device_type='1' admin_state='1'
  condition_flags='2359296' part_num='800-00103-r05'
  observed_flags='0' eqid='123456104988' control_bits='0'>
    <channel channel_type='1' condition_flags='16'
    observed_flags='0' eqid='123456104988.1' control_bits='0' />
  </device>
  <device image_bits='131072' device_type='1' admin_state='1'
  condition_flags='2097152' part_num='800-00103-r05'
  observed_flags='0' eqid='123456103294' control_bits='0'>
    <channel channel_type='1' condition_flags='0'
    observed_flags='0' eqid='123456103294.1' control_bits='0' />
  </device>
  <device image_bits='131072' device_type='1' admin_state='1'
  condition_flags='2359296' part_num='800-00103-r05'
  observed_flags='0' eqid='123456105346' control_bits='0'>
    <channel channel_type='1' condition_flags='16'
    observed_flags='0' eqid='123456105346.1' control_bits='0' />
  </device>
  <device image_bits='131072' device_type='1' admin_state='1'
  condition_flags='2359296' part_num='800-00103-r05'
  observed_flags='0' eqid='123456105297' control_bits='0'>
    <channel channel_type='1' condition_flags='16'
    observed_flags='0' eqid='123456105297.1' control_bits='0' />
  </device>
  <device image_bits='131072' device_type='1' admin_state='1'
  condition_flags='2359296' part_num='800-00103-r05'
  observed_flags='0' eqid='123456104896' control_bits='0'>
    <channel channel_type='1' condition_flags='16'
    observed_flags='0' eqid='123456104896.1' control_bits='0' />
  </device>
  <device image_bits='131072' device_type='1' admin_state='1'
  condition_flags='2097152' part_num='800-00103-r05'
  observed_flags='0' eqid='123456105331' control_bits='0'>
    <channel channel_type='1' condition_flags='0'
    observed_flags='0' eqid='123456105331.1' control_bits='0' />
  </device>
  <device image_bits='131072' device_type='1' admin_state='1'
  condition_flags='2359296' part_num='800-00103-r05'
  observed_flags='0' eqid='123456103546' control_bits='0'>
    <channel channel_type='1' condition_flags='16'
    observed_flags='0' eqid='123456103546.1' control_bits='0' />
  </device>
  <device image_bits='131072' device_type='1' admin_state='1'
  condition_flags='2097152' part_num='800-00103-r05'
  observed_flags='0' eqid='123456103685' control_bits='0'>
    <channel channel_type='1' condition_flags='0'
    observed_flags='0' eqid='123456103685.1' control_bits='0' />
  </device>
  <device image_bits='131072' device_type='1' admin_state='1'
  condition_flags='2359296' part_num='800-00103-r05'
  observed_flags='0' eqid='123456103215' control_bits='0'>
    <channel channel_type='1' condition_flags='16'
    observed_flags='0' eqid='123456103215.1' control_bits='0' />
  </device>
  <device image_bits='131072' device_type='1' admin_state='1'
  condition_flags='2097152' part_num='800-00103-r05'
  observed_flags='0' eqid='123456102635' control_bits='0'>
    <channel channel_type='1' condition_flags='0'
    observed_flags='0' eqid='123456102635.1' control_bits='0' />
  </device>
  <device image_bits='131072' device_type='1' admin_state='1'
  condition_flags='2097152' part_num='800-00103-r05'
  observed_flags='0' eqid='123456105296' control_bits='0'>
    <channel channel_type='1' condition_flags='0'
    observed_flags='0' eqid='123456105296.1' control_bits='0' />
  </device>
</perf_report>

The response headers look like this:

HTTP/1.1 200 OK
Date: Mon, 13 Feb 2012 11:56:48 GMT
Server: Apache
X-Powered-By: Phusion Passenger (mod_rails/mod_rack) 3.0.11
ETag: "95bc2eaddfac613540f469946f28af4b"
X-Runtime: 297
Cache-Control: private, max-age=0, must-revalidate
Content-Length: 142
Status: 200
Cache-Control: max-age=31536000
Expires: Tue, 12 Feb 2013 11:56:48 GMT
Connection: close
Content-Type: application/x-deflate; charset=utf-8

And the response body -- after inflating it -- looks something like this:

<?xml version="1.0" encoding="UTF-8"?>
<perf_report_response status="success">
  <events_processed>
    <event id="221"/>
    <event id="222"/>
    <event id="223"/>
    <event id="224"/>
    <event id="225"/>
    <event id="226"/>
    <event id="227"/>
    <event id="228"/>
  </events_processed>
</perf_report_response>

My goal is to set up a simple proxy that in addition to passing the information along to Enphase will also collect it locally. I'll update the blog if I make progress on that front.

Monday, December 26, 2011

Very simple rate limiting

I use CrashPlan as a backup service.  It works and is very simple to set up, but has limited options for controlling bandwidth.  In fact, if you're running it on a headless system (e.g., a fileserver of some sort), your options are effectively "too slow" and "CONSUME EVERYTHING".

There is an open request to add time-based limitations to the application itself, but for now I've solved this using a very simple traffic shaping configuration.  Because the learning curve for "tc" and friends is surprisingly high, I'm putting my script here in the hopes that other people might find it useful, and so that I can find it when I need to do this again someday.

#!/bin/sh


# The network device used for backups
dev=p10p1


# The remove address of the CrashPlanserver
crashplan_addr=50.93.246.1


# The port
crashplan_port=443


# The rate limit. See tc(8) for acceptable syntax.
crashplan_limit=2mbit

if [ "$1" = "enable" ]; then
    #
    # This creates and activates the traffic shaper
    # configuration.
    #
    logger -s -t ratelimit -p user.notice "enabling rate limits"
    tc qdisc del dev $dev root > /dev/null 2>&1
    tc qdisc add dev $dev root handle 1: htb
    tc class add dev $dev parent 1: classid 1:10 htb rate $crashplan_limit
    tc filter add dev $dev parent 1: prio 0 protocol ip handle 10 fw flowid 1:10
    iptables -t mangle -A OUTPUT -d $crashplan_addr -p tcp --dport $crashplan_port -j MARK --set-mark 10
elif [ "$1" = "disable" ]; then
    #
    # This removes the traffic shaper 
    # configuration.
    #
    logger -s -t ratelimit -p user.notice "disabling rate limits"
    tc qdisc del dev $dev root > /dev/null 2>&1
    iptables -t mangle -D OUTPUT -d $crashplan_addr -p tcp --dport $crashplan_port -j MARK --set-mark 10
elif [ "$1" = "show" ]; then
    #
    # Shows the current traffic shaper configuration.
    #
    tc qdisc show dev $dev
    tc class show dev $dev
    tc filter show dev $dev
    iptables -t mangle -vnL OUTPUT
fi

Tuesday, August 16, 2011

Puppet, scope, and inheritance

I note this here because it wasn't apparent to me from the Puppet documentation.

If you have a Puppet class like this:

class foo {
  File {  ensure  => file,
          mode    => 600,
          }
}

And you use it like this:

class bar {
  include foo

  file { '/tmp/myfile': }
}

Then /tmp/myfile will not be created. But if instead you do this:

class bar inherits foo {
  file { '/tmp/myfile': }
}

It will be created with mode 0600. In other words, if you use inherits then definitions in the parent class are available in the scope of your subclass. If you include, then definitions in he included class are "below" the scope of the including class.

Tuesday, July 26, 2011

Fixing rpmsign with evil magic

At my office we are developing a deployment mechanism for RPM packages. The general workflow looks like this:

  • You build a source rpm on your own machine.
  • You sign the rpm with your GPG key.
  • You submit the source RPM to our buildserver.
  • The buildserver validates your signature and then builds the package.
  • The buildserver signs the package using a master signing key.

The last step in that sequence represents a problem, because the rpmsign command will always, always prompt for a password and read the response from /dev/tty. This means that (a) you can't easily provide the password on stdin, and (b) you can't fix the problem using a passwordless key.

Other people have solved this problem using expect, but I've opted for another solution which in some ways seems cleaner and in others seems like a terrible idea: function interposition using LD_PRELOAD.

The rpmsign command prompts for (and reads) a password using the getpass() function call. If you look at the getpass(3) man page, you'll see that the function is defined like this:

       #include 
       char *getpass( const char *prompt);

So we start with the following short block of C code:

#include 
#include 

char *getpass( const char *prompt) {
 printf("I ATE YOUR PASSPHRASE.\n");
 return "";
}

This -- when properly loaded -- will replace the standard C library getpass() function with our own version, which simply returns an empty string. This of course means we'll be using a passwordless key, but you could obviously have our replacement function return an actual password instead of an empty string. I would argue that by doing so you would not substantially increase the security of your solution.

Next we create a shared library:

$ cc -fPIC -g   -c -o getpass.o getpass.c
$ ld -shared -o getpass.so getpass.o

And now we perform our magic:

$ LD_PRELOAD=$(pwd)/getpass.so rpmsign --addsign some.src.rpm
I ATE YOUR PASSPHRASE.
Pass phrase is good.

And voila! A solution for operating rpmsign in batch mode.

Sunday, May 22, 2011

Installing CrashPlan under FreeBSD 8

This articles describes how I got CrashPlan running on my FreeBSD 8(-STABLE) system. These instructions by Kim Scarborough were my starting point, but as these were for FreeBSD 7 there were some additional steps necessary to get things working.

Install Java

I had originally thought that it might be possible to run the CrashPlan client "natively" under FreeBSD. CrashPlan is a Java application, so this seemed like a possible solution. Unfortunately, Java under FreeBSD 8 seems to be a lost cause. I finally gave up and just installed Java under Linux.

Set up your Linux compatability environment

The simplest way to do this is to follow the instructions in the FreeBSD Handbook. This will get you a Fedora 10 based Linux userspace, which should be more than sufficient. I'm using a CentOS 5.6 userspace, but for what we're doing it shouldn't matter, modulo some minor differences in paths.

Note that Linux software running in this environment will have a modified view of your filesystem. In particular, /etc will map to /compat/linux/etc, and ZFS filesystems with non-default mountpoints seem to behave oddly (they are accessible, but not necessarily visible before you access them). This may require some workarounds in CrashPlan, depending on what you're trying to back up.

Install Java JRE

I installed a compatible Java environment from the CentOS package repository:

# chroot /compat/linux bash
bash-3.2# yum install java-1.6.0-openjdk
bash-3.2# exit

Install CrashPlan

Install the CrashPlan software

  • Download CrashPlan for Linux

  • Unpack the archive (named something like CrashPlan_3.0.3_Linux.tgz)

  • Change to the CrashPlan-install directory.

  • Run the following commands:

    # export PATH=/compat/linux/usr/lib/jvm/jre-1.6.0-openjdk/bin:$PATH
    # /compat/linux/bin/bash install.sh
    
  • Install CrashPlan into /usr/local. When prompted for where to locate init scripts ("What directory contains your SYSV init scripts?" and "What directory contains your runlevel init links?"), enter /tmp (because the installed init scripts aren't ideal for your FreeBSD environment -- we'll install our own later on).

Fix Java

The Linux runtime provided by the FreeBSD Linux compatability layer does not include all of the features of recent Linux kernels. In particular, it is missing the epoll* syscalls, which will cause Java to die with a Function not implemented error. The workaround for this is documented in the linux-kernel page on the FreeBSD wiki:

If you run an application in the linux java which wants to use the linux epoll functions (you should see "not implemented" messages in dmesg), you can start java with the argument -Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.PollSelectorProvider

Install an rc script

Place the following script into /usr/local/etc/rc.d/crashplan:

#!/bin/sh
#

# PROVIDE: crashplan
# REQUIRE: NETWORKING
# KEYWORD: shutdown

. /etc/rc.subr

name="crashplan"
rcvar=`set_rcvar`
start_cmd=crashplan_start
stop_cmd=crashplan_stop

crashplan_start () {
  /compat/linux/bin/bash /usr/local/crashplan/bin/CrashPlanEngine start
}

crashplan_stop () {
  /compat/linux/bin/bash /usr/local/crashplan/bin/CrashPlanEngine stop
}

load_rc_config $name
run_rc_command "$1"

And then add:

crashplan_enable="YES"

To /etc/rc.conf (or /etc/rc.conf.local).

Start CrashPlan

Run:

/usr/local/etc/rc.d/crashplan start

Wait a moment, then run:

/compat/linux/bin/bash /usr/local/crashplan/bin/CrashPlanEngine status

This should verify that CrashPlan is running.

Connect CrashPlan client

Follow the instructions provided by CrashPlan for connecting to a headless CrashPlan desktop.

Monday, May 9, 2011

Signing data with ssh-agent

This is follow-up to my previous post, Converting OpenSSH public keys.

OpenSSH allows one to use an agent that acts as a proxy to your private key. When using an agent -- particularly with agent forwarding enabled -- this allows you to authenticate to a remote host without having to (a) repeatedly type in your password or (b) expose an unencrypted private key to remote systems.

If one is temtped to use SSH keys as authentication credentials outside of ssh, one would ideally be able to take advantage of the ssh agent for these same reasons.

This article discusses what is required to programmatically interact with the agent and with the OpenSSL libraries for signing data and verifying signatures.

Signing data with ssh-agent

The SSH agent does not provide clients with direct access to an unencrypted private key. Rather, it will accept data from the client and return the signature of the SHA1 hash of the data.

The agent communicates over a unix socket using the ssh agent protocol defined in authfd.h. The Python Paramiko libary (a pure-python implementation of ssh) includes support for interacting with an ssh agent.

Signing data is very simple:

import hashlib
import paramiko.agent

data = 'something to sign'
data_sha1 = hashlib.sha1(data).digest()
a = paramiko.agent.Agent()
key = a.keys[0]
d = key.sign_ssh_data(None, data_sha1)

Internally, the agent computes the SHA1 digest for the data, signs this using the selected key, and returns a signature_blob that varies depending on the key type in use. For an RSA signature, the result format is a series of (length, data) pairs, where the length is encoded as a four-byte unsigned integer. The response contains the following elements:

  1. algorithm name (ssh-rsa)
  2. rsa signature

For example, after signing some data using a 1024-bit private key, the value returned from sign_ssh_data looked like this:

0000000: 0000 0007 7373 682d 7273 6100 0000 8027  ....ssh-rsa....'
0000010: 953c 771c 5ee4 f4b0 9849 c061 0ac2 2adb  .<w.^....I.a..*.
0000020: b53d 2bcb a545 8dbb d582 05e5 a916 6490  .=+..E........d.
0000030: 1b67 3210 9bfc c74d d0ad 5011 394b a3fe  .g2....M..P.9K..
0000040: 96e2 910b bbfd 19cd 73e5 6720 503a 95e1  ........s.g P:..
0000050: 5b8b 63c4 14a3 ec3d bf57 846e f0b4 e66c  [.c....=.W.n...l
0000060: ce5d 6327 6055 b4e2 3c14 c13f 8303 4b1a  .]c'`U..<..?..K.
0000070: 7ce3 9f33 9e7c 7ca4 a97b 506d fa0b a39e  |..3.||..{Pm....
0000080: cb53 befc d725 9cd1 a8af 6042 5ac8 01    .S...%....`BZ..

The first four bytes (0000 0007) are the length of the algorithm name (ssh-rsa). The next field is the length of the signature (0000 0080, or 128 bytes), followed by the signature data. This means we can extract the signature data like this:

parts = []
while d:
    len = struct.unpack('>I', d[:4])[0]
    bits = d[4:len+4]
    parts.append(bits)
    d = d[len+4:]

sig = parts[1]
open('signature', 'w').write(sig)

Signing the data with OpenSSL

Using M2Crypto

You can accomplish the same thing using the M2Crypto library for Python like this:

import hashlib
import M2Crypto.RSA

data = 'something to sign'
data_sha1 = hashlib.sha1(data).digest()
key = M2Crypto.RSA.load_key('testkey')
sig = key.sign(data_sha1)
open('signature', 'w').write(sig)

This assumes that testkey is the private key file corresponding to the first key loaded into your agent in the previous example.

Using the command line

You can also generate an equivalent signature using the OpenSSL command line tools:

echo -n 'something to sign' |
  openssl sha1  -binary |
  openssl pkeyutl -sign -inkey testkey -pkeyopt digest:sha1 > signature

Note that including -pkeyopt digest:sha1 is necessary to get a signature block that is compatible with the one returned by the ssh agent. The pkeyutl man page has this to say:

In PKCS#1 padding if the message digest is not set then the supplied data is signed or verified directly instead of using a DigestInfo structure. If a digest is set then the a DigestInfo structure is used and its the length must correspond to the digest type.

Veryfying the data

You can verify the signature using the corresponding public key.

Using M2Crypto

This uses the M2Crypto module to verify the signature computed in the previous step:

import hashlib
import M2Crypto.RSA

# let's pretend that you've read my previous blog post and have
# created an "sshkey" module for reading the ssh public key format.
import sshkey

data = 'something to sign'
data_sha1 = hashlib.sha1(data).digest()

# read the signature generated in the previous step
sig = open('signature').read()

e,n = sshkey.load_rsa_pub_key('testkey.pub')
key = M2Crypto.RSA.new_pub_key((
    M2Crypto.m2.bn_to_mpi(M2Crypto.m2.hex_to_bn(hex(e)[2:])),
    M2Crypto.m2.bn_to_mpi(M2Crypto.m2.hex_to_bn(hex(n)[2:])),
    ))

if key.verify(data_sha1, sig):
  print 'Verified!'
else:
  print 'Failed!'

If you have converted the ssh public key into a standard format, you could do this instead:

import hashlib
import M2Crypto.RSA

data = 'something to sign'
data_sha1 = hashlib.sha1(data).digest()

# read the signature generated in the previous step
sig = open('signature').read()

key = M2Crypto.RSA.load_pub_key('testkey.pubssl')

if key.verify(data_sha1, sig):
  print 'Verified!'
else:
  print 'Failed!'
Using OpenSSL

We can do the same thing on the command line, but we'll first need to convert the ssh public key into a format useful to OpenSSL. This is easy if you have the private key handy...which we do:

openssl rsa -in testkey -pubout > testkey.pubssl

And now we can verify the signature:

echo 'something to sign' |
  openssl sha1  -binary |
  openssl pkeyutl -verify -sigfile signature \
    -pubin -inkey testkey.pubssl -pkeyopt digest:sha1