lp-signing

A service for storing keys and signing messages.

Development environment

Create a bionic LXD container:

$ lxc launch ubuntu:bionic lp-signing-bionic

(You may want to use a profile to bind-mount your home directory as well.)

From now on, instructions will assume you’re inside the project’s LXD container, unless otherwise noted.

Install dependencies in the container:

$ sudo ./setup-container

Bootstrap the project:

$ make bootstrap

Run the tests:

$ make test

Generate a key pair, and add the private half to service_private_keys (JSON-encoded) in the [auth] section of service.conf:

$ env/bin/lp-signing generate-key-pair

Generate another key pair, and add the private half to private_keys (JSON-encoded) in the [key_storage] section of service.conf:

$ env/bin/lp-signing generate-key-pair

If you want to communicate with the lp-signing service via a client application, you need a keypair for it. The keypair can be generated using the env/bin/lp-signing generate-keypair command, which prints the public and private keys of the generated keypair. The public key from that output can be registered using the env/bin/lp-signing register-client <client-name> <client-public-key> command, where <client-public-key> is a base64-encoded NaCl public key, and the private key has to be used in the client application. Please check the example client code below in this document for more details.

Note

In the case of the Launchpad development instance acting as a client, the keypair can be obtained from configs/development/launchpad-lazr.conf in the Launchpad repository and the public key registered using this command.

Start the server:

$ make run

Using the service

This is still rather rough; we need a proper client library.

Find the IP address of the system where the signing service is running, e.g. using lxc list.

Sample code for generating and signing keys then looks something like this:

import base64
import json

from nacl.encoding import Base64Encoder
from nacl.public import (
    Box,
    PrivateKey,
    PublicKey,
    )
from nacl.utils import random
import requests

# Replace the IP address here with your signing service's IP address.
base_url = "http://10.36.63.25:8000/"

response = requests.get(f"{base_url}/service-key")
response.raise_for_status()
service_key = PublicKey(
    response.json()["service-key"], encoder=Base64Encoder)

private_key = PrivateKey.generate()
encoded_public_key = (
    private_key.public_key.encode(encoder=Base64Encoder).decode("UTF-8"))
print(
    f"Run 'env/bin/lp-signing register-client test-client "
    f"{encoded_public_key}' to register a test client.")

def get_nonce():
    response = requests.post(f"{base_url}/nonce")
    response.raise_for_status()
    return base64.b64decode(response.json()["nonce"].encode("UTF-8"))

def make_response_nonce():
    return random(Box.NONCE_SIZE)

def decrypt_response_json(response, response_nonce):
    box = Box(private_key, service_key)
    return json.loads(box.decrypt(
        response.content, response_nonce, encoder=Base64Encoder))

def generate(key_type, description):
    nonce = get_nonce()
    response_nonce = make_response_nonce()
    data = json.dumps({
        "key-type": key_type,
        "description": description,
        }).encode("UTF-8")
    box = Box(private_key, service_key)
    encrypted_message = box.encrypt(data, nonce, encoder=Base64Encoder)
    response = requests.post(
        f"{base_url}/generate",
        headers={
            "Content-Type": "application/x-boxed-json",
            "X-Client-Public-Key": private_key.public_key.encode(
                encoder=Base64Encoder).decode("UTF-8"),
            "X-Nonce": encrypted_message.nonce.decode("UTF-8"),
            "X-Response-Nonce": (
                Base64Encoder.encode(response_nonce).decode("UTF-8")),
            },
        data=encrypted_message.ciphertext)
    response.raise_for_status()
    return decrypt_response_json(response, response_nonce)

def sign(key_type, fingerprint, message_name, message, mode):
    nonce = get_nonce()
    response_nonce = make_response_nonce()
    data = json.dumps({
        "key-type": key_type,
        "fingerprint": fingerprint,
        "message-name": message_name,
        "message": base64.b64encode(message).decode("UTF-8"),
        "mode": mode,
        }).encode("UTF-8")
    box = Box(private_key, service_key)
    encrypted_message = box.encrypt(data, nonce, encoder=Base64Encoder)
    response = requests.post(
        f"{base_url}/sign",
        headers={
            "Content-Type": "application/x-boxed-json",
            "X-Client-Public-Key": private_key.public_key.encode(
                encoder=Base64Encoder).decode("UTF-8"),
            "X-Nonce": encrypted_message.nonce.decode("UTF-8"),
            "X-Response-Nonce": (
                Base64Encoder.encode(response_nonce).decode("UTF-8")),
            },
        data=encrypted_message.ciphertext)
    response.raise_for_status()
    return decrypt_response_json(response, response_nonce)

response_json = generate("UEFI", "test")
fingerprint = response_json["fingerprint"]

with open("/path/to/test/image", "rb") as f:
    response_json = sign(
        "UEFI", fingerprint, "test-image", f.read(), "ATTACHED")
with open("/path/to/test/image.signed", "wb") as f:
    f.write(base64.b64decode(response_json["signed-message"]))

How-to guides