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"]))