Mon 07 May 2012

How does BrowserID work?

This article will explain how cryptography behind BrowserID works, and lightly cover why BrowserID is a better alternative to OpenID.

A website will ask your browser for a BrowserID assertion via JavaScript. This will either use a native BrowserID API in your browser, or it will use the JavaScript implementation of BrowserID. This is known as the user agent.

navigator.id.get(gotAssertion);

For supporting browsers, the user agent will be supplied and the navigator.id.get method will exist. The user agent can be provided by either the browser, an extension to the browser, or via a JavaScript include on the website to use a JavaScript implementation.

<script src="https://browserid.org/include.js" type="text/javascript"></script>

This means that a browser or browser extension can provide their own implementation, or a JavaScript implementation can be used in unsupported browsers.

Out of the process, once the browser has an assertion. It will provide the assertion to the gotAssertion callback. The website can then provide this back to the web server to validate the assertion to confirm the user's identity.

What is an assertion?

An assertion is a string which holds a JSON structure including an identity assertion and an identity certificate. The assertion will be generated by the browser for an identity, the browser will need to get an identity certificate from a provider to ensure they own that identity. This identity can be used again and again until it expires.

Identity certificate

An identity certificate will look like the following:

{
    "iss": "kylefuller.co.uk",
    "exp": "1313971280961",
    "public-key": {
        ...
    },
    "principal": {
        "email": "inbox@kylefuller.co.uk"
    }
}

The "iss" field will include the domain of the issuer. The code checking the assertion should verify that is either the domain used in the email address, or a trusted fallback provider, such as browserid.org. This means that either the domain providing your email address or browserid.org can provide a valid identity certificate for your email.

The fallback provider will allow you to use that provider instead of your email provider in the situation that your email provider does not support BrowserID.

The above identity certificate signed by the issuer, and their certificate should be available over HTTPS at https://iss/.well-known/browserid.

The public key in the identity certificate is the public key of the user. This key can be generated by the browser so only the user will have control over it.

Identity assertion

The identity assertion is the final piece in a BrowserID assertion. It will be signed by the browser's private key, so it will match the public key in the identity certificate which our provider has confirmed we own.

{
    "exp": 1320280579437,
    "aud": "https://demand-browserid.herokuapp.com"
}

The identity assertion provides an expiry and an audience. The identity assertion allows a website to confirm the assertion is meant for them, the audience should be the website that we want to sign-in to.

The relying party which the user is identifying to should check both the identity certificate and the identity assertion to check the certificates match, and that neither has expired. If these are valid assertions for our audience, then we can confirm the user owns the email address and in the identity certificate.

Why use BrowserID, whats wrong with OpenID?

There are many benefits of BrowserID over OpenID.

  • BrowserID is far simpler to use than OpenID for an average user. It is also quicker, since you do not need to type your OpenID into each site.
  • When you sign-in to a site using OpenID, your OpenID provider knows you are visiting that site. With BrowserID, your BrowserID provider is not aware of the site you are visiting since your assertion is created in the browser. You have already retrieved your identity certificate from the provider. Your privacy is protected with BrowserID.
  • BrowserID uses an email address, any existing site will already have your email address. This would allow a site to start using BrowserID without requiring you to provide any additional information.
Wed 28 March 2012

Organising dotfiles in a git repository

Organising dotfiles can be done in numerous ways. Many dotfile repositories often have their own clunky script to copy or symbolically link their dotfiles in place. I feel this is a dirty approach and I prefer my files to be easily manageable via the git command. I don't want to have to copy a file every time I change it.

/static/images/dotfiles.png

Another approach I have seen done, is making your whole home directory a git repository. Unfortunately after using this solution you will come across a number of flaws, any repository in your home directory will now follow your ~/.gitignore. I also came into many problems when I was using Xcode which will try to git add projects into your dotfiles repository. Like symbolic linking all my dotfiles into place, this solution also felt clunky.

Git allows you to seperate the work tree and the git dir via environment variables or arguments to the git command. This allows us to store the bare git dir in ~/.files.git while still keeping our entire home directory as the work tree for git.

I settled for using a simple alias in my .zshrc which allows me to easily use git to manage my dotfiles. But, it is imporant to remember that the home directory will not be seen as a git repository unless you use this alias. You won't be able to accidentally use git commands thinking you were in another repo, (or accidentally git add a bunch of things in a mercurial repository).

Here is the alias I use:

$ alias home=git --work-tree=$HOME --git-dir=$HOME/.files.git

You can use this alias just as you would use the normal git command, this allows you to clone and init a repo. To clone a dotfiles repo, you can do:

$ home clone git://github.com/kylef/dotfiles.git
Sun 18 March 2012

Website 2.0

It's taken me nearly a year to finish my new website. I have gone through many iterations and have finally stuck with this theme and system.

Instead of running my own custom Django blog, this site is now powered by Pelican, a static site generator. I have created numerous patches for Pelican so that it works with custom URL structures, so none of my URL's break when I migrated to Pelican.

But this isn't your second revision of the site?

Ok you got me, this isn't really my second version of this site. I don't know how many revisions I have gone through, but this is the second revision to have the same content and posts. It's my second major release of the site in a version control system. I use a git repository for the current site which can be found on github here.

What is new?

The theme for the website is completely different, I have made a new about page. I have lost all the comments from my old site, there was far too much spam to port them over.

/static/images/website-20.png
Sat 13 August 2011

Monitoring InspIRCd with MRTG

This guide will show you how to configure MRTG to show statistics from an InspIRCd IRC server, this will include user counts and channel counts. This guide assumes you already have InspIRCd and MRTG setup and working. You can see an example of this working at Sector 5D.

InspIRCd config

First, you will need to configure InspIRCd to use the m_http and m_http_stats module. I configure InspIRCd to listen on localhost and port 8081 with SSL, but you could pick any port you want, just remember to change the $address variable inside inspircd-stats.sh later.

Note, the following config is for InspIRCd 2.0, if you use a older version you may need to change the bind line to a http line, please see the InspIRCd wiki for using a older version.

<module name="m_httpd.so">
<module name="m_httpd_stats.so">
<bind address="localhost" port="8081" type="httpd" ssl="gnutls">

Once you have placed this in your InspIRCd config, you can rehash or reload InspIRCd and then you will be able to connect to it over http(s) to retrive stats. Such as at https://localhost:8081/stats

Creating a script to gather the user or channel count on InspIRCd

The following script queries the InspIRCd m_httpd_stats module for the user or channel count. This depends on XMLStarlet (Debian package, Arch AUR Package), so please install this first.

#!/bin/sh

address="https://localhost:8081/stats"
count_type=$1

if [[ "$count_type" != "user"  && "$count_type" != "channel" ]]; then
  echo "Usage: $0 <user|channel>"
  exit 1
elif

count=$(wget -q -Y off -O - --no-check-certificate $address | xml sel -t -v "inspircdstats/general/${count_type}count")

echo $count
echo $count
echo "IRC $count_type"

This script will need to be available from the user which you run MRTG as. I have placed mine in /usr/local/bin/inspircd-stats.sh but you could place it somewhere like $HOME/bin/inspircd-stats.sh.

To download and install this to /usr/local/bin run the following:

sudo mkdir -p /usr/local/bin
sudo wget https://raw.github.com/gist/1042604/b19a4cfd91b6956063d962c977f3d5f8e3318d7d/inspircd-stats.sh -O /usr/local/bin/inspircd-stats.sh
sudo chmod+x /usr/local/bin/inspircd-stats.sh

Now you have this inspircd-stats.sh script, you can use it to get the user and channel count as follows:

/usr/local/bin/inspircd-stats.sh user
/usr/local/bin/inspircd-stats.sh channel

MRTG Config

You will need to add the following to your mrtg config file, this could be located at /etc/mrtg.cfg, but this depends on how you setup mrtg.

# For IRC Users:
Target[irc.users]: `/usr/local/bin/inspircd-stats.sh user`
Title[irc.users]: IRC Users
PageTop[irc.users]: <h1>IRC Users</h1>
MaxBytes[irc.users]: 10000000000
ShortLegend[irc.users]: users
YLegend[irc.users]: Users
LegendI[irc.users]: Users
LegendO[irc.users]:
Legend1[irc.users]: Users
Legend2[irc.users]:
Options[irc.users]: growright,nopercent,gauge

# For IRC Channels:
Target[irc.channels]: `/usr/local/bin/inspircd-stats.sh channel`
Title[irc.channels]: IRC Channels
PageTop[irc.channels]: <h1>IRC Channels</h1>
MaxBytes[irc.channels]: 10000000000
ShortLegend[irc.channels]: channels
YLegend[irc.channels]: Channels
LegendI[irc.channels]: Channels
LegendO[irc.channels]:
Legend1[irc.channels]: Channels
Legend2[irc.channels]:
Options[irc.channels]: growright,nopercent,gauge

Now, you can run your MRTG command for your config or wait for the MRTG cron and it should show up your InspIRCd stats.

Wed 01 July 2009

Pre-populating data in the admin panel

I have always found it awkward working with sites and user's in django admin panel. James Bennet from b-list.org explained his way of doing it on his blog, but I found his way a bit limiting, I still want superusers to be able to change the author of a post.

After looking around in the source code for ModelAdmin I found two method's, one of which was not documented. These were formfield_for_manytomany and formfield_for_foreignkey. These methods allow us to supply our own FormField, which could have initial data. These methods also get passed the HttpRequest, which contains the user. So we can fill in author of a blog post and the current site.

In this post I will quickly show you how to use this method to fill in current data based upon the request inside the admin.

My model

Here is a model from which the rest of the article will work from.

from django.db import models
from django.contrib.sites.models import Site
from django.contrib.auth.models import User

class Post(models.Model):
    title = models.CharField(max_length=255)
    content = models.TextField(blank=True)

    sites = models.ManyToManyField(Site)
    author = models.ForeignKey(User)

As you can see, the author is a ForeignKey field, and sites is a ManyToMany field. The methods we use to set the default reflects this. We use formfield_for_foreignkey to set the default user.

Here is my ModelAdmin class

class PostAdmin(admin.ModelAdmin):
    fieldsets = (
    (None, {
        'fields': ('title', 'content',)
    }),
    ('Advanced options', {
        'classes': ('collapse',),
        'fields': ('author', 'sites',)
    })
)

admin.site.register(Post, PostAdmin)

Selecting the current site

To select the current site, what we will do is create a method called formfield_for_manytomany inside our PostAdmin class. This method will be called each time the admin panel goes to draw a ManyToMany field in a Post. So all we do is set the initial value to the current site. But it is important to remember that the field may be something else, so it is important to check if it is the sites field.

def formfield_for_manytomany(self, db_field, request, **kwargs):
    if db_field.name == 'sites':
        kwargs['initial'] = [Site.objects.get_current()]
        return db_field.formfield(**kwargs)

    return super(PostAdmin, self).formfield_for_manytomany(db_field, request, **kwargs)

Selecting the current user

This is very similar to selecting the current site, instead we create a method called formfield_for_foreignkey for ForeignKey's instead of ManyToMany fields.

def formfield_for_foreignkey(self, db_field, request, **kwargs):
    if db_field.name == 'author':
        kwargs['initial'] = request.user.id
        return db_field.formfield(**kwargs)

    return super(PostAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)

Now that we have selected the current user, it would be nice to only let superusers use this field, but I couldn't find a way to only show this for certain users. So instead we will just disallow normal user's from saving the post as another user.

I dont think it is possible to optionally hide the author field from a user, so what I have done was to disallow a non superuser from editing the post's author.

Disallowing a user from changing another post

def has_change_permission(self, request, obj=None):
    has_class_permission = super(PostAdmin, self).has_change_permission(request, obj)

    if not has_class_permission:
        return False

    if obj is not None and not request.user.is_superuser and request.user.id != obj.author.id:
        return False

This snippet was not written by myself, but by James Bennet from b-list.org.

Only let the user view their own posts

Another measure to limit the possibilities of the user can be to let the user only view their own posts. To do this we just change the queryset to filter posts where the author is themselves. We still want superusers to see all posts, so only filter it for normal users.

def queryset(self, request):
    if request.user.is_superuser:
        return Post.objects.all()
    return Post.objects.filter(author=request.user)

To see a complete example of this you can view the source code for lithium.