diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000000000000000000000000000000000000..f306504d9b8ee44e34836f331f287b2e268fc55a
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+*.feature gitlab-language=gherkin
diff --git a/app/controllers/api/keys_controller.rb b/app/controllers/api/keys_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7eb76ee675f7c5fde6c13289c91ecd8e713adc25
--- /dev/null
+++ b/app/controllers/api/keys_controller.rb
@@ -0,0 +1,75 @@
+class Api::KeysController < ApiController
+
+  before_filter :require_login
+  before_filter :require_enabled
+
+  # get /keys
+  def index
+    keys = identity.keys.map do |k,v|
+      [k, JSON.parse(v)]
+    end
+    render json: keys.to_h
+  end
+
+  def show
+    render json: JSON.parse(identity.keys[params[:id]])
+  end
+
+  def create
+    keyring.create type, value
+    head :no_content
+  rescue Keyring::Error, ActionController::ParameterMissing => e
+    render status: 422, json: {error: e.message}
+  end
+
+  def update
+    keyring.update type, rev: rev, value: value
+    head :no_content
+  rescue Keyring::NotFound => e
+    render status: 404, json: {error: e.message}
+  rescue Keyring::Error, ActionController::ParameterMissing => e
+    render status: 422, json: {error: e.message}
+  end
+
+  def destroy
+    keyring.delete type, rev: rev
+    head :no_content
+  rescue Keyring::NotFound => e
+    render status: 404, json: {error: e.message}
+  rescue Keyring::Error, ActionController::ParameterMissing => e
+    render status: 422, json: {error: e.message}
+  end
+
+
+  protected
+
+  def require_enabled
+    if !current_user.enabled?
+      access_denied
+    end
+  end
+
+  def service_level
+    current_user.effective_service_level
+  end
+
+  def type
+    params.require :type
+  end
+
+  def value
+    params.require :value
+  end
+
+  def rev
+    params.require :rev
+  end
+
+  def keyring
+    @keyring ||= Keyring.new identity
+  end
+
+  def identity
+    @identity ||= Identity.for(current_user)
+  end
+end
diff --git a/app/models/identity.rb b/app/models/identity.rb
index 92f8f7a039680b10a6ecfbd4da0e4f10170d3fc6..b8c2245630b9d2672d18cbf567396a306140df6f 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -136,6 +136,11 @@ class Identity < CouchRest::Model::Base
     write_attribute('keys', keys.merge(type => key.to_s))
   end
 
+  def delete_key(type)
+    raise 'key not found' unless keys[type]
+    write_attribute('keys', keys.except(type))
+  end
+
   def cert_fingerprints
     read_attribute('cert_fingerprints') || Hash.new
   end
diff --git a/app/models/keyring.rb b/app/models/keyring.rb
new file mode 100644
index 0000000000000000000000000000000000000000..66f7bfdbaf08b379078d48200170938ebf8c52ed
--- /dev/null
+++ b/app/models/keyring.rb
@@ -0,0 +1,55 @@
+#
+# Keyring
+#
+# A collection of cryptographic keys.
+#
+
+class Keyring
+  class Error < RuntimeError
+  end
+
+  class NotFound < Error
+    def initialize(type)
+      super "no such key: #{type}"
+    end
+  end
+
+  def initialize(storage)
+    @storage = storage
+  end
+
+  def create(type, value)
+    raise Error, "key already exists" if storage.keys[type].present?
+    storage.set_key type, {type: type, value: value, rev: new_rev}.to_json
+    storage.save
+  end
+
+  def update(type, rev:, value:)
+    check_rev type, rev
+    storage.set_key type, {type: type, value: value, rev: new_rev}.to_json
+    storage.save
+  end
+
+  def delete(type, rev:)
+    check_rev type, rev
+    storage.delete_key type
+    storage.save
+  end
+
+  def key_of_type(type)
+    JSON.parse(storage.keys[type]) if storage.keys[type]
+  end
+
+  protected
+  attr_reader :storage
+
+  def check_rev(type, rev)
+    old = key_of_type(type)
+    raise NotFound, type unless old
+    raise Error, "wrong revision: #{rev}" unless old['rev'] == rev
+  end
+
+  def new_rev
+    SecureRandom.urlsafe_base64(8)
+  end
+end
diff --git a/config/routes.rb b/config/routes.rb
index d3d2cec732587eaab171c9686f3104f0189a0b2e..55d03faf52ee9a5e4adf3e224e460d28018d6fb2 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -41,6 +41,7 @@ LeapWeb::Application.routes.draw do
     resource :service, :only => [:show]
     resources :configs, :only => [:index, :show]
     resources :identities, :only => [:show]
+    resources :keys, :except=> [:edit, :new]
   end
 
   scope "(:locale)", :locale => CommonLanguages.match_available do
diff --git a/features/2/keys.feature b/features/2/keys.feature
new file mode 100644
index 0000000000000000000000000000000000000000..83e70e7e73e79d4b6cc81b2543d94c0da72a4a16
--- /dev/null
+++ b/features/2/keys.feature
@@ -0,0 +1,257 @@
+Feature: Handle current users collection of keys
+
+  LEAP currently uses OpenPGP and is working on implementing katzenpost.
+  Both systems require public keys of a user to be available for retrival.
+
+  The /2/keys endpoint allows the client to manage the public keys
+  registered for their users email address.
+
+  You need to specify the type of the key when publishing it. Some
+  keytypes such as 'openpgp' and 'katzenpost_id' will only allow a
+  single key to be published. Others such as 'katzenpost_link' allow
+  multiple keys to be registered at the same time. We deal with this
+  by allowing arbitrary json data to be specified as the value of the
+  key. So katzenpost_link keys can be combined in a json data structure.
+
+  POST request will register a new key. In order to replace an existing
+  key you need to send a PATCH request to /keys/:type including the last
+  revision (rev) of the key. This way we can detect conflicts between
+  concurrend updates.
+
+  Background:
+    Given I authenticated
+    Given I set headers:
+      | Accept       | application/json |
+      | Content-Type | application/json |
+      | Authorization | Token token="MY_AUTH_TOKEN" |
+
+  Scenario: Get initial empty set of keys
+    When I send a GET request to "2/keys"
+    Then the response status should be "200"
+    And the response should be:
+    """
+      {}
+    """
+
+  Scenario: Get all the keys
+    Given I have published a "openpgp" key
+    And I have published "katzenpost_link" keys
+    When I send a GET request to "2/keys"
+    Then the response status should be "200"
+    And the response should be:
+    """
+    {
+    "openpgp": {
+      "type": "openpgp",
+      "value": "DUMMY_KEY",
+      "rev": "DUMMY_REV"
+      },
+    "katzenpost_link": {
+      "type": "katzenpost_link",
+      "value": {
+        "one": "DUMMY_KEY",
+        "two": "DUMMY_KEY"
+      },
+      "rev": "DUMMY_REV"
+      }
+    }
+    """
+
+  Scenario: Get a single key
+    Given I have published a "openpgp" key
+    When I send a GET request to "2/keys/openpgp"
+    Then the response status should be "200"
+    And the response should be:
+    """
+    {
+      "type": "openpgp",
+      "value": "DUMMY_KEY",
+      "rev": "DUMMY_REV"
+    }
+    """
+
+  Scenario: Get a set of keys for one type
+    Given I have published "katzenpost_link" keys
+    When I send a GET request to "2/keys/katzenpost_link"
+    Then the response status should be "200"
+    And the response should be:
+    """
+      {
+        "type": "katzenpost_link",
+        "value": {
+          "one": "DUMMY_KEY",
+          "two": "DUMMY_KEY"
+          },
+        "rev": "DUMMY_REV"
+      }
+    """
+
+  Scenario: Publish an initial OpenPGP key
+    When I send a POST request to "2/keys" with the following:
+    """
+      {
+      "type": "openpgp",
+      "value": "ASDF"
+      }
+    """
+    Then the response status should be "204"
+    And I should have published a "openpgp" key
+
+  Scenario: Do not overwrite an existing key
+    Given I have published a "openpgp" key
+    When I send a POST request to "2/keys" with the following:
+    """
+      {
+      "type": "openpgp",
+      "value": "QWER"
+      }
+    """
+    Then the response status should be "422"
+    And the response should be:
+    """
+      {
+      "error": "key already exists"
+      }
+    """
+
+  Scenario: Publishing an empty key fails
+    When I send a POST request to "2/keys" with the following:
+    """
+      {}
+    """
+    Then the response status should be "422"
+    And the response should be:
+    """
+      {
+      "error": "param is missing or the value is empty: type"
+      }
+    """
+
+  Scenario: Updating an existing key
+    Given I have published a "openpgp" key
+    When I send a PATCH request to "2/keys/openpgp" with the following:
+    """
+      {
+      "type": "openpgp",
+      "value": "QWER",
+      "rev": "DUMMY_REV"
+      }
+    """
+    Then the response status should be "204"
+    And I should have published a "openpgp" key with value "QWER"
+
+  Scenario: Updating a missing key raises
+    When I send a PATCH request to "2/keys/openpgp" with the following:
+    """
+      {
+      "type": "openpgp",
+      "value": "QWER",
+      "rev": "DUMMY_REV"
+      }
+    """
+    Then the response status should be "404"
+    And the response should be:
+    """
+      {
+      "error": "no such key: openpgp"
+      }
+    """
+    And I should not have published a "openpgp" key
+
+  Scenario: Updating an existing key require revision
+    Given I have published a "openpgp" key
+    When I send a PATCH request to "2/keys/openpgp" with the following:
+    """
+      {
+      "type": "openpgp",
+      "value": "QWER"
+      }
+    """
+    Then the response status should be "422"
+    And the response should be:
+    """
+      {
+      "error": "param is missing or the value is empty: rev"
+      }
+    """
+
+  Scenario: Updating an existing key require right revision
+    Given I have published a "openpgp" key
+    When I send a PATCH request to "2/keys/openpgp" with the following:
+    """
+      {
+      "type": "openpgp",
+      "value": "QWER",
+      "rev": "WRONG_REV"
+      }
+    """
+    Then the response status should be "422"
+    And the response should be:
+    """
+      {
+      "error": "wrong revision: WRONG_REV"
+      }
+    """
+
+  Scenario: Deleting an existing key
+    Given I have published a "openpgp" key
+    When I send a DELETE request to "2/keys/openpgp" with the following:
+    """
+      {
+      "type": "openpgp",
+      "rev": "DUMMY_REV"
+      }
+    """
+    Then the response status should be "204"
+    And I should not have published a "openpgp" key
+
+  Scenario: Deleting a missing key raises
+    When I send a DELETE request to "2/keys/openpgp" with the following:
+    """
+      {
+      "type": "openpgp",
+      "rev": "DUMMY_REV"
+      }
+    """
+    Then the response status should be "404"
+    And the response should be:
+    """
+      {
+      "error": "no such key: openpgp"
+      }
+    """
+
+  Scenario: Deleting an existing key require revision
+    Given I have published a "openpgp" key
+    When I send a DELETE request to "2/keys/openpgp" with the following:
+    """
+      {
+      "type": "openpgp"
+      }
+    """
+    Then the response status should be "422"
+    And the response should be:
+    """
+      {
+      "error": "param is missing or the value is empty: rev"
+      }
+    """
+    And I should have published a "openpgp" key
+
+  Scenario: Deleting an existing key require right revision
+    Given I have published a "openpgp" key
+    When I send a DELETE request to "2/keys/openpgp" with the following:
+    """
+      {
+      "type": "openpgp",
+      "rev": "WRONG_REV"
+      }
+    """
+    Then the response status should be "422"
+    And the response should be:
+    """
+      {
+      "error": "wrong revision: WRONG_REV"
+      }
+    """
+    And I should have published a "openpgp" key
diff --git a/features/step_definitions/api_steps.rb b/features/step_definitions/api_steps.rb
index 71886948d39654e5deb0c45217954660223f915b..7b732721e2e9250e41d15045644b52b6637cbcf1 100644
--- a/features/step_definitions/api_steps.rb
+++ b/features/step_definitions/api_steps.rb
@@ -37,7 +37,7 @@ When /^I digest\-authenticate as the user "(.*?)" with the password "(.*?)"$/ do
   digest_authorize user, pass
 end
 
-When /^I (?:have sent|send) a (GET|POST|PUT|DELETE) request (?:for|to) "([^"]*)"(?: with the following:)?$/ do |*args|
+When /^I (?:have sent|send) a (GET|POST|PUT|DELETE|PATCH) request (?:for|to) "([^"]*)"(?: with the following:)?$/ do |*args|
   request_type = args.shift
   path = args.shift
   input = args.shift
@@ -45,7 +45,7 @@ When /^I (?:have sent|send) a (GET|POST|PUT|DELETE) request (?:for|to) "([^"]*)"
   request_opts = {method: request_type.downcase.to_sym}
 
   unless input.nil?
-    if input.class == Cucumber::Ast::Table
+    if input.class == Cucumber::MultilineArgument::DataTable
       request_opts[:params] = input.rows_hash
     else
       request_opts[:input] = input
diff --git a/features/step_definitions/key_steps.rb b/features/step_definitions/key_steps.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3d5e015492985b743e8af67fa3a33b41bfc5f6d9
--- /dev/null
+++ b/features/step_definitions/key_steps.rb
@@ -0,0 +1,26 @@
+Given /^I have published a "([^"]*)" key$/ do |type|
+  identity = Identity.for(@user)
+  keyring = Keyring.new(identity)
+  SecureRandom.stubs(urlsafe_base64: 'DUMMY_REV')
+  keyring.create type, 'DUMMY_KEY'
+end
+
+Given /^I have published "([^"]*)" keys$/ do |type|
+  identity = Identity.for(@user)
+  keyring = Keyring.new(identity)
+  SecureRandom.stubs(urlsafe_base64: 'DUMMY_REV')
+  keyring.create type, one: 'DUMMY_KEY', two: 'DUMMY_KEY'
+end
+
+Then /^I should have published an? "([^"]*)" key(?: with value "([^"]*)")?$/ do |type, value|
+  identity = Identity.for(@user)
+  keys = identity.keys
+  assert_includes keys.keys, type
+  assert_equal value, JSON.parse(keys[type])['value'] if value
+end
+
+Then /^I should not have published an? "([^"]*)" key$/ do |type|
+  identity = Identity.for(@user)
+  keys = identity.keys
+  refute_includes keys.keys, type
+end
diff --git a/features/support/env.rb b/features/support/env.rb
index d3067dbd8f1ebba1ac1a6c2760dcd718d3e6ab3a..d722b8ed6874b31930f9c00b50c20118576ee77e 100644
--- a/features/support/env.rb
+++ b/features/support/env.rb
@@ -55,4 +55,4 @@ end
 # The :transaction strategy is faster, but might give you threading problems.
 # See https://github.com/cucumber/cucumber-rails/blob/master/features/choose_javascript_database_strategy.feature
 Cucumber::Rails::Database.javascript_strategy = :truncation
-
+require 'mocha/setup'
diff --git a/test/unit/identity_test.rb b/test/unit/identity_test.rb
index 6836487e3abf1755df4a37525291ed008107bee0..43f644ada755db1f63e5adc88a0e2e56f82e2904 100644
--- a/test/unit/identity_test.rb
+++ b/test/unit/identity_test.rb
@@ -80,6 +80,14 @@ class IdentityTest < ActiveSupport::TestCase
     assert_equal pgp_key_string, @id.keys[:pgp]
   end
 
+  test "deleting pgp key" do
+    @id = Identity.for(@user)
+    @id.set_key(:pgp, pgp_key_string)
+    @id.delete_key(:pgp)
+    assert_nil @id.keys[:pgp]
+    assert_equal Hash.new, @id.keys
+  end
+
   test "querying pgp key via couch" do
     @id = Identity.for(@user)
     @id.set_key(:pgp, pgp_key_string)
diff --git a/test/unit/keyring_test.rb b/test/unit/keyring_test.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c7df63e948722eedf671035a9ff4fa34e0624b65
--- /dev/null
+++ b/test/unit/keyring_test.rb
@@ -0,0 +1,100 @@
+require 'test_helper'
+
+class KeyringTest < ActiveSupport::TestCase
+
+  test 'create initial key' do
+    keyring.create 'type', 'value'
+    assert_equal 'value', keyring.key_of_type('type')['value']
+  end
+
+  test 'raise on creating twice' do
+    keyring.create 'type', 'value'
+    assert_raises Keyring::Error do
+      keyring.create 'type', 'value'
+    end
+  end
+
+  test 'update with new key' do
+    keyring.create 'type', 'value'
+    initial_rev = keyring.key_of_type('type')['rev']
+    keyring.update 'type', rev: initial_rev, value: 'new value'
+    assert_equal 'new value', keyring.key_of_type('type')['value']
+  end
+
+  test 'raise on updating missing key' do
+    assert_raises Keyring::NotFound do
+      keyring.update 'type', rev: nil ,value: 'new value'
+    end
+    assert_nil keyring.key_of_type('type')
+  end
+
+  test 'raise on updating without rev' do
+    keyring.create 'type', 'value'
+    assert_raises Keyring::Error do
+      keyring.update 'type', rev: nil ,value: 'new value'
+    end
+    assert_equal 'value', keyring.key_of_type('type')['value']
+  end
+
+  test 'raise on updating with wrong rev' do
+    keyring.create 'type', 'value'
+    assert_raises Keyring::Error do
+      keyring.update 'type', rev: 'wrong rev', value: 'new value'
+    end
+    assert_equal 'value', keyring.key_of_type('type')['value']
+  end
+
+  test 'delete key' do
+    keyring.create 'type', 'value'
+    initial_rev = keyring.key_of_type('type')['rev']
+    keyring.delete 'type', rev: initial_rev
+    assert_nil keyring.key_of_type('type')
+  end
+
+  test 'raise on deleting missing key' do
+    assert_raises Keyring::NotFound do
+      keyring.delete 'type', rev: nil
+    end
+  end
+
+  test 'raise on deleting without rev' do
+    keyring.create 'type', 'value'
+    assert_raises Keyring::Error do
+      keyring.delete 'type', rev: nil
+    end
+    assert_equal 'value', keyring.key_of_type('type')['value']
+  end
+
+  test 'raise on deleting with wrong rev' do
+    keyring.create 'type', 'value'
+    assert_raises Keyring::Error do
+      keyring.delete 'type', rev: 'wrong rev'
+    end
+    assert_equal 'value', keyring.key_of_type('type')['value']
+  end
+
+
+  protected
+
+  def keyring
+    @keyring ||= Keyring.new(teststorage)
+  end
+
+  def teststorage
+    @teststorage ||= Hash.new.tap do |dummy|
+      def dummy.set_key(type, value)
+        self[type] = value
+      end
+
+      def dummy.keys
+        self
+      end
+
+      def dummy.delete_key(type)
+        self.delete(type)
+      end
+
+      def dummy.save; end
+    end
+  end
+end