Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for signing with PKCS11 keys #157

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ jobs:
- name: Build Server Container
run: docker build -t server:latest -f Dockerfile .

- name: Build PKCS11 Server Container
run: docker build -t server_pkcs11:latest -f DockerfileServerPKCS11Test .

- name: Build Client Container
run: docker build -t client:latest -f DockerfileClientTest .

Expand Down
49 changes: 49 additions & 0 deletions DockerfileServerPKCS11Test
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Dockerfile for square/sharkey (server).
#
# Building:
# docker build --rm -t square/sharkey-server .
#
# Basic usage:
# docker run -e SHARKEY_CONFIG=/path/to/config -e SHARKEY_MIGRATIONS=/path/to/migration/dir square/sharkey-server
#
# This image only contains the server component of sharkey,
# the client will have to be deployed separately

FROM golang:1.18-alpine as build

# Install CGO deps
RUN apk add --update gcc musl-dev && \
rm -rf /var/cache/apk/*

WORKDIR /app

COPY go.mod .
COPY go.sum .

# Download dependencies
RUN go mod download

# Copy source
COPY . .

# Build & set-up
RUN cp test/integration/pkcs11_entry.sh /usr/bin/entrypoint.sh && \
cp test/integration/softhsm_setup.sh /usr/bin/softhsm_setup.sh && \
chmod +x /usr/bin/entrypoint.sh && \
chmod +x /usr/bin/softhsm_setup.sh && \
go build -buildvcs=false -o /usr/bin/sharkey-server github.com/square/sharkey/cmd/sharkey-server

# Create a multi-stage build with the binary
FROM golang:1.18-alpine

COPY --from=build /usr/bin/sharkey-server /usr/bin/sharkey-server
COPY --from=build /usr/bin/entrypoint.sh /usr/bin/entrypoint.sh
COPY --from=build /usr/bin/softhsm_setup.sh /usr/bin/softhsm_setup.sh

RUN apk update && apk add bash && apk add openssl && apk add curl && \
rm -rf /var/cache/apk/*
RUN mkdir -p /sharkey/ca_pub_keys/
RUN apk add softhsm && apk add opensc
RUN /usr/bin/softhsm_setup.sh

ENTRYPOINT ["/usr/bin/entrypoint.sh"]
7 changes: 7 additions & 0 deletions examples/server.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ tls:
# Signing key (from ssh-keygen)
signing_key: /path/to/ca-signing-key

# Optional PKCS11 configuration. If using PKCS11 key for signing, all values must be specified, otherwise 'signing_key' will be used for signing
pkcs11:
lib_path: /usr/lib/softhsm/libsofthsm2.so
pin_path: /build/test/integration/pkcs11_pin
token_label: sharkey-test-token-ec_prime256v1
pub_key_path: /sharkey/ca_pub_keys/pkcs11_ec_prime256v1_pub_key.pem

# Lifetime/validity duration for generated host certificates
host_cert_duration: 168h

Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ require (
github.com/hashicorp/go-immutable-radix v1.2.0 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28 // indirect
github.com/letsencrypt/pkcs11key/v4 v4.0.0 // indirect
github.com/lib/pq v1.8.0 // indirect
github.com/miekg/pkcs11 v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f // indirect
github.com/ziutek/mymysql v0.0.0-20160623123511-8787d5581eb6 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,15 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28 h1:mkl3tvPHIuPaWsLtmHTybJeoVEW7cbePK73Ir8VtruA=
github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28/go.mod h1:T/T7jsxVqf9k/zYOqbgNAsANsjxTd1Yq3htjDhQ1H0c=
github.com/letsencrypt/pkcs11key/v4 v4.0.0 h1:qLc/OznH7xMr5ARJgkZCCWk+EomQkiNTOoOF5LAgagc=
github.com/letsencrypt/pkcs11key/v4 v4.0.0/go.mod h1:EFUvBDay26dErnNb70Nd0/VW3tJiIbETBPTl9ATXQag=
github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg=
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/pkcs11 v1.0.2 h1:CIBkOawOtzJNE0B+EpRiUBzuVW7JEQAwdwhSS6YhIeg=
github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
Expand Down
72 changes: 61 additions & 11 deletions integration-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function die() {

function wait_for_container() {
echo "Waiting for $1..."
for i in range 0 20; do
for i in {1..20}; do
if docker ps | grep -q "$1"; then
return
fi
Expand All @@ -25,31 +25,64 @@ function wait_for_container() {
function cleanup() {
echo "Cleanup..."
docker logs server
docker logs server_pkcs11_ec_spec284r1
docker logs server_pkcs11_ec_prime256v1
docker logs client
docker stop server_pkcs11_ec_spec284r1 -t 20 || die "failed to stop 'server_pkcs11'"
docker stop server_pkcs11_ec_prime256v1 -t 20 || die "failed to stop 'server_pkcs11'"
docker stop client -t 20 || die "failed to stop 'client'"
docker stop server -t 20 || die "failed to stop 'server'"
rm -r "$TMPDIR" || die "failed to remove '$TMPDIR'"
}

function start_server() {
docker run -d --rm \
--name=$1 \
-v "$PWD":"$BUILD_CONTEXT" \
-e SHARKEY_CONFIG=$2 \
-e SHARKEY_MIGRATIONS="$MIGRATION_CONFIG" \
-p $3:8080 \
$4 start

wait_for_container $1
}

function test_server() {
curl --cert $PWD/test/tls/proxy.crt --key $PWD/test/tls/proxy.key \
https://localhost:$1/enroll_user -H "X-Forwarded-User: alice" \
-d @$PWD/test/ssh/alice_rsa.pub -k \
-o $TMPDIR/alice_rsa-cert.pub -sS

ssh-keygen -L -f $TMPDIR/alice_rsa-cert.pub

# Extract CA public key (Private key generated in "hardware")
docker cp $2:/sharkey/ca_pub_keys/$3 $TMPDIR/

# Convert to openssh format
CA_PUB_KEY_PEM_FORMAT=$3
CA_PUB_KEY_OPENSSH_FORMAT=${CA_PUB_KEY_PEM_FORMAT%.*}.pub
ssh-keygen -i -m PKCS8 -f $TMPDIR/$CA_PUB_KEY_PEM_FORMAT > $TMPDIR/$CA_PUB_KEY_OPENSSH_FORMAT

# Copy to client container
docker cp $TMPDIR/$CA_PUB_KEY_OPENSSH_FORMAT client:/etc/ssh/ca_user_key.pub

ssh -v -p 14296 -o "BatchMode yes" -o "UserKnownHostsFile=test/integration/known_hosts" -i $TMPDIR/alice_rsa alice@client true || die "failed to connect to 'client'"
ssh -v -p 14296 -o "BatchMode yes" -o "UserKnownHostsFile=test/integration/known_hosts" -i $TMPDIR/alice_rsa alice@localhost true || die "failed to connect to 'localhost'"
}

trap cleanup EXIT

BUILD_CONTEXT=/build
SERVER_CONFIG="${BUILD_CONTEXT}/test/integration/server_config.yaml"
SERVER_PKCS11_EC_PRIME256V1_CA_CONFIG="${BUILD_CONTEXT}/test/integration/server_pkcs11_ec_prime256v1_ca_config.yaml"
SERVER_PKCS11_EC_SPEC384R1_CA_CONFIG="${BUILD_CONTEXT}/test/integration/server_pkcs11_ec_spec384r1_ca_config.yaml"
MIGRATION_CONFIG="${BUILD_CONTEXT}/db/sqlite"
CLIENT_CONFIG="${BUILD_CONTEXT}/test/integration/client_config.yaml"

echo Starting sharkey server container

# Start server
docker run -d --rm \
--name=server \
-v "$PWD":"$BUILD_CONTEXT" \
-e SHARKEY_CONFIG="$SERVER_CONFIG" \
-e SHARKEY_MIGRATIONS="$MIGRATION_CONFIG" \
-p 12321:8080 \
server start

wait_for_container server
start_server server $SERVER_CONFIG 12321 server

echo Starting sharkey client container

Expand All @@ -72,7 +105,7 @@ echo "Starting integration test"

echo "Signing user ssh key"
# Sign user ssh key
# NOTE: on MacOS ensure that your curl it built with openssl (and not SecureTransport)
# NOTE: on MacOS ensure that your curl is built with openssl (and not SecureTransport)
# or you won't be able to load client cert from PEM file
curl --cert $PWD/test/tls/proxy.crt --key $PWD/test/tls/proxy.key \
https://localhost:12321/enroll_user -H "X-Forwarded-User: alice" \
Expand All @@ -94,3 +127,20 @@ fi
echo Attempting to ssh into client container
ssh -v -p 14296 -o "BatchMode yes" -o "UserKnownHostsFile=test/integration/known_hosts" -i $TMPDIR/alice_rsa alice@client true || die "failed to connect to 'client'"
ssh -v -p 14296 -o "BatchMode yes" -o "UserKnownHostsFile=test/integration/known_hosts" -i $TMPDIR/alice_rsa alice@localhost true || die "failed to connect to 'localhost'"

declare -a test1=("server_pkcs11_ec_prime256v1" "$SERVER_PKCS11_EC_PRIME256V1_CA_CONFIG" "12421" "server_pkcs11" "pkcs11_ec_prime256v1_pub_key.pem")
declare -a test2=("server_pkcs11_ec_spec284r1" "$SERVER_PKCS11_EC_SPEC384R1_CA_CONFIG" "12521" "server_pkcs11" "pkcs11_ec_spec384r1_pub_key.pem")
declare -a pkcs11_tests=(test1 test2)

for args in "${pkcs11_tests[@]}"
do
declare -n lst="$args"
container_name=${lst[0]}
sharkey_config=${lst[1]}
container_port=${lst[2]}
image_name=${lst[3]}
ca_pub_key_filename=${lst[4]}

start_server $container_name $sharkey_config $container_port $image_name
test_server $container_port $container_name $ca_pub_key_filename
done
5 changes: 1 addition & 4 deletions pkg/client/sharkey_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,10 +256,7 @@ func (c *Client) shellOut(command []string) {
if c.conf.Sudo != "" {
command = append([]string{c.conf.Sudo}, command...)
}
cmd := exec.Cmd{
Path: command[0],
Args: command,
}
cmd := exec.Command(command[0], command[1:]...)
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &stdout
Expand Down
19 changes: 19 additions & 0 deletions pkg/server/api/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
package api

import (
"crypto/ecdsa"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io"
"io/ioutil"
"math/big"
"net/http"
"net/http/httptest"
"os"
Expand Down Expand Up @@ -308,6 +310,23 @@ func TestStatus(t *testing.T) {
"Expected Content-Type to be set to 'application/json', but instead got %s", rec.Header().Get("Content-Type"))
}

func TestGetPubKeyFromFile(t *testing.T) {
pub, err := getPubKeyFromFile("testdata/server_ca_prime256v1_pub.pem")
require.NoError(t, err)

expectedPubXComponent := "ffff236e99785f8578b3fe355f71fe14bb88557fcd4c3c0fe9a9f7c67236e518"
expectedPubYComponent := "40317d4b903c170336612d041fd6eb78378b6f60bb9538df11b5c3065d7f59ca"
expectedX := new(big.Int)
expectedY := new(big.Int)
expectedX.SetString(expectedPubXComponent, 16)
expectedY.SetString(expectedPubYComponent, 16)

ecpub, ok := pub.(*ecdsa.PublicKey)
require.True(t, ok, "Public key isn't a valid ECDSA key")
require.Equalf(t, expectedX, ecpub.X, "EC Public key X component unexpectedly parsed as: '%v', expected: '%v'", fmt.Sprintf("%x", ecpub.X), expectedPubXComponent)
require.Equalf(t, expectedY, ecpub.Y, "EC Public key Y component unexpectedly parsed as: '%v', expected: '%v'", fmt.Sprintf("%x", ecpub.Y), expectedPubYComponent)
}

func generateContext(t *testing.T) (*Api, error) {
conf := &config.Config{
SigningKey: "testdata/server_ca",
Expand Down
56 changes: 49 additions & 7 deletions pkg/server/api/sharkey.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,17 @@
package api

import (
"crypto"
"crypto/x509"
"encoding/json"
"github.com/square/sharkey/pkg/server/cert"
"encoding/pem"
"fmt"
"io/ioutil"
"net/http"

"github.com/letsencrypt/pkcs11key/v4"
"github.com/square/sharkey/pkg/server/cert"

_ "bitbucket.org/liamstask/goose/lib/goose"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
Expand Down Expand Up @@ -51,18 +57,37 @@ type Api struct {

func Run(conf *config.Config, logger *logrus.Logger) {
logger.Print("Starting http server")
privateKey, err := ioutil.ReadFile(conf.SigningKey)
if err != nil {
logger.WithError(err).Fatal("unable to read signing key file")
}

storage, err := storage.FromConfig(conf.Database)
if err != nil {
logger.WithError(err).Fatal("unable to setup database")
}
defer storage.Close()

sshSigner, err := ssh.ParsePrivateKey(privateKey)
var sshSigner ssh.Signer
if conf.PKCS11.LibPath != "" && conf.PKCS11.PinPath != "" && conf.PKCS11.TokenLabel != "" && conf.PKCS11.PubKeyPath != "" {
pubKey, err := getPubKeyFromFile(conf.PKCS11.PubKeyPath)
if err != nil {
logger.WithError(err).Fatal("error retrieving public key from file:", err)
}
p11Pin, err := ioutil.ReadFile(conf.PKCS11.PinPath)
if err != nil {
logger.WithError(err).Fatal("unable to read signing key file")
}
p11key, err := pkcs11key.New(conf.PKCS11.LibPath, conf.PKCS11.TokenLabel, string(p11Pin), pubKey)
if err != nil {
logger.WithError(err).Fatal("error creating pkcs11 key object:", err)
}
sshSigner, err = ssh.NewSignerFromSigner(p11key)
if err != nil {
logger.WithError(err).Fatal("error creating ssh signer backed by pkcs11 key:", err)
}
} else {
privateKey, err := ioutil.ReadFile(conf.SigningKey)
if err != nil {
logger.WithError(err).Fatal("unable to read signing key file")
}
sshSigner, err = ssh.ParsePrivateKey(privateKey)
}
if err != nil {
logger.WithError(err).Fatal("unable to parse signing key data")
}
Expand Down Expand Up @@ -150,3 +175,20 @@ func (c *Api) Status(w http.ResponseWriter, r *http.Request) {
}
_, _ = w.Write(out)
}

func getPubKeyFromFile(filepath string) (crypto.PublicKey, error) {
pubPEM, err := ioutil.ReadFile(filepath)
if err != nil {
return nil, fmt.Errorf("error reading public key from file: %s", err)
}
block, _ := pem.Decode([]byte(pubPEM))
if block == nil {
return nil, fmt.Errorf("error parsing PEM block containing public key")
}
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse DER encoded public key: %s", err)
}

return pub, nil
}
4 changes: 4 additions & 0 deletions pkg/server/api/testdata/server_ca_prime256v1_pub.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE//8jbpl4X4V4s/41X3H+FLuIVX/N
TDwP6an3xnI25RhAMX1LkDwXAzZhLQQf1ut4N4tvYLuVON8RtcMGXX9Zyg==
-----END PUBLIC KEY-----
8 changes: 8 additions & 0 deletions pkg/server/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type Config struct {
Database Database `yaml:"db"`
TLS TLS `yaml:"tls"`
SigningKey string `yaml:"signing_key"`
PKCS11 PKCS11 `yaml:"pkcs11"`
HostCertDuration string `yaml:"host_cert_duration"`
UserCertDuration string `yaml:"user_cert_duration"`
ListenAddr string `yaml:"listen_addr"`
Expand All @@ -49,6 +50,13 @@ type TLS struct {
Key string
}

type PKCS11 struct {
LibPath string `yaml:"lib_path"`
PinPath string `yaml:"pin_path"`
TokenLabel string `yaml:"token_label"`
PubKeyPath string `yaml:"pub_key_path"`
}

type Database struct {
Username string
Password string
Expand Down
15 changes: 15 additions & 0 deletions test/integration/pkcs11_entry.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/bin/sh

## Docker container entry point

if [ -z "$SHARKEY_CONFIG" ]; then
echo "No configuration file specified, aborting."
exit 1
fi

if ! [ -z "$SHARKEY_MIGRATIONS" ]; then
/usr/bin/sharkey-server migrate --config="$SHARKEY_CONFIG" --migrations="$SHARKEY_MIGRATIONS"
fi

# Start the server
exec /usr/bin/sharkey-server --config="$SHARKEY_CONFIG" "$@"
1 change: 1 addition & 0 deletions test/integration/pkcs11_pin
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1234
Loading