mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-24 01:41:42 +00:00
cmd/dist,release/dist: sign QNAP builds with a Google Cloud hosted key
QNAP now requires builds to be signed with an HSM. This removes support for signing with a local keypair. This adds support for signing with a Google Cloud hosted key. The key should be an RSA key with protection level `HSM` and that uses PSS padding and a SHA256 digest. The GCloud project, keyring and key name are passed in as command-line arguments. The GCloud credentials and the PEM signing certificate are passed in as Base64-encoded command-line arguments. Updates tailscale/corp#23528 Signed-off-by: Percy Wegmann <percy@tailscale.com>
This commit is contained in:
parent
0c78f081a4
commit
26f31f73f4
25
cmd/dist/dist.go
vendored
25
cmd/dist/dist.go
vendored
@ -5,11 +5,13 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"slices"
|
||||||
|
|
||||||
"tailscale.com/release/dist"
|
"tailscale.com/release/dist"
|
||||||
"tailscale.com/release/dist/cli"
|
"tailscale.com/release/dist/cli"
|
||||||
@ -19,9 +21,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
synologyPackageCenter bool
|
synologyPackageCenter bool
|
||||||
qnapPrivateKeyPath string
|
gcloudCredentialsBase64 string
|
||||||
qnapCertificatePath string
|
gcloudProject string
|
||||||
|
gcloudKeyring string
|
||||||
|
qnapKeyName string
|
||||||
|
qnapCertificateBase64 string
|
||||||
)
|
)
|
||||||
|
|
||||||
func getTargets() ([]dist.Target, error) {
|
func getTargets() ([]dist.Target, error) {
|
||||||
@ -42,10 +47,11 @@ func getTargets() ([]dist.Target, error) {
|
|||||||
// To build for package center, run
|
// To build for package center, run
|
||||||
// ./tool/go run ./cmd/dist build --synology-package-center synology
|
// ./tool/go run ./cmd/dist build --synology-package-center synology
|
||||||
ret = append(ret, synology.Targets(synologyPackageCenter, nil)...)
|
ret = append(ret, synology.Targets(synologyPackageCenter, nil)...)
|
||||||
if (qnapPrivateKeyPath == "") != (qnapCertificatePath == "") {
|
qnapSigningArgs := []string{gcloudCredentialsBase64, gcloudProject, gcloudKeyring, qnapKeyName, qnapCertificateBase64}
|
||||||
return nil, errors.New("both --qnap-private-key-path and --qnap-certificate-path must be set")
|
if cmp.Or(qnapSigningArgs...) != "" && slices.Contains(qnapSigningArgs, "") {
|
||||||
|
return nil, errors.New("all of --gcloud-credentials, --gcloud-project, --gcloud-keyring, --qnap-key-name and --qnap-certificate must be set")
|
||||||
}
|
}
|
||||||
ret = append(ret, qnap.Targets(qnapPrivateKeyPath, qnapCertificatePath)...)
|
ret = append(ret, qnap.Targets(gcloudCredentialsBase64, gcloudProject, gcloudKeyring, qnapKeyName, qnapCertificateBase64)...)
|
||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,8 +60,11 @@ func main() {
|
|||||||
for _, subcmd := range cmd.Subcommands {
|
for _, subcmd := range cmd.Subcommands {
|
||||||
if subcmd.Name == "build" {
|
if subcmd.Name == "build" {
|
||||||
subcmd.FlagSet.BoolVar(&synologyPackageCenter, "synology-package-center", false, "build synology packages with extra metadata for the official package center")
|
subcmd.FlagSet.BoolVar(&synologyPackageCenter, "synology-package-center", false, "build synology packages with extra metadata for the official package center")
|
||||||
subcmd.FlagSet.StringVar(&qnapPrivateKeyPath, "qnap-private-key-path", "", "sign qnap packages with given key (must also provide --qnap-certificate-path)")
|
subcmd.FlagSet.StringVar(&gcloudCredentialsBase64, "gcloud-credentials", "", "base64 encoded GCP credentials (used when signing QNAP builds)")
|
||||||
subcmd.FlagSet.StringVar(&qnapCertificatePath, "qnap-certificate-path", "", "sign qnap packages with given certificate (must also provide --qnap-private-key-path)")
|
subcmd.FlagSet.StringVar(&gcloudProject, "gcloud-project", "", "name of project in GCP KMS (used when signing QNAP builds)")
|
||||||
|
subcmd.FlagSet.StringVar(&gcloudKeyring, "gcloud-keyring", "", "path to keyring in GCP KMS (used when signing QNAP builds)")
|
||||||
|
subcmd.FlagSet.StringVar(&qnapKeyName, "qnap-key-name", "", "name of GCP key to use when signing QNAP builds")
|
||||||
|
subcmd.FlagSet.StringVar(&qnapCertificateBase64, "qnap-certificate", "", "base64 encoded certificate to use when signing QNAP builds")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
18
release/dist/qnap/files/scripts/Dockerfile.qpkg
vendored
18
release/dist/qnap/files/scripts/Dockerfile.qpkg
vendored
@ -1,9 +1,21 @@
|
|||||||
FROM ubuntu:20.04
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
RUN apt-get update -y && \
|
RUN apt-get update -y && \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
git-core \
|
git-core \
|
||||||
ca-certificates
|
ca-certificates \
|
||||||
RUN git clone https://github.com/qnap-dev/QDK.git
|
apt-transport-https \
|
||||||
|
gnupg \
|
||||||
|
curl \
|
||||||
|
patch
|
||||||
|
|
||||||
|
# Install QNAP QDK (force a specific version to pick up updates)
|
||||||
|
RUN git clone https://github.com/tailscale/QDK.git && cd /QDK && git reset --hard 9a31a67387c583d19a81a378dcf7c25e2abe231d
|
||||||
RUN cd /QDK && ./InstallToUbuntu.sh install
|
RUN cd /QDK && ./InstallToUbuntu.sh install
|
||||||
ENV PATH="/usr/share/QDK/bin:${PATH}"
|
ENV PATH="/usr/share/QDK/bin:${PATH}"
|
||||||
|
|
||||||
|
# Install Google Cloud PKCS11 module
|
||||||
|
RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
|
||||||
|
RUN echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||||
|
RUN apt-get update -y && apt-get install -y --no-install-recommends google-cloud-cli libengine-pkcs11-openssl
|
||||||
|
RUN curl -L https://github.com/GoogleCloudPlatform/kms-integrations/releases/download/pkcs11-v1.6/libkmsp11-1.6-linux-amd64.tar.gz | tar xz
|
||||||
|
40
release/dist/qnap/files/scripts/sign-qpkg.sh
vendored
Executable file
40
release/dist/qnap/files/scripts/sign-qpkg.sh
vendored
Executable file
@ -0,0 +1,40 @@
|
|||||||
|
#! /usr/bin/env bash
|
||||||
|
set -xeu
|
||||||
|
|
||||||
|
mkdir -p "$HOME/.config/gcloud"
|
||||||
|
echo "$GCLOUD_CREDENTIALS_BASE64" | base64 --decode > /root/.config/gcloud/application_default_credentials.json
|
||||||
|
gcloud config set project "$GCLOUD_PROJECT"
|
||||||
|
|
||||||
|
echo "---
|
||||||
|
tokens:
|
||||||
|
- key_ring: \"$GCLOUD_KEYRING\"
|
||||||
|
log_directory: "/tmp/kmsp11"
|
||||||
|
" > pkcs11-config.yaml
|
||||||
|
chmod 0600 pkcs11-config.yaml
|
||||||
|
|
||||||
|
export KMS_PKCS11_CONFIG=`readlink -f pkcs11-config.yaml`
|
||||||
|
export PKCS11_MODULE_PATH=/libkmsp11-1.6-linux-amd64/libkmsp11.so
|
||||||
|
|
||||||
|
# Verify signature of pkcs11 module
|
||||||
|
# See https://github.com/GoogleCloudPlatform/kms-integrations/blob/master/kmsp11/docs/user_guide.md#downloading-and-verifying-the-library
|
||||||
|
echo "-----BEGIN PUBLIC KEY-----
|
||||||
|
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEtfLbXkHUVc9oUPTNyaEK3hIwmuGRoTtd
|
||||||
|
6zDhwqjJuYaMwNd1aaFQLMawTwZgR0Xn27ymVWtqJHBe0FU9BPIQ+SFmKw+9jSwu
|
||||||
|
/FuqbJnLmTnWMJ1jRCtyHNZawvv2wbiB
|
||||||
|
-----END PUBLIC KEY-----" > pkcs11-release-signing-key.pem
|
||||||
|
openssl dgst -sha384 -verify pkcs11-release-signing-key.pem -signature "$PKCS11_MODULE_PATH.sig" "$PKCS11_MODULE_PATH"
|
||||||
|
|
||||||
|
echo "$QNAP_SIGNING_CERT_BASE64" | base64 --decode > cert.crt
|
||||||
|
|
||||||
|
openssl cms \
|
||||||
|
-sign \
|
||||||
|
-binary \
|
||||||
|
-nodetach \
|
||||||
|
-engine pkcs11 \
|
||||||
|
-keyform engine \
|
||||||
|
-inkey "pkcs11:object=$QNAP_SIGNING_KEY_NAME" \
|
||||||
|
-keyopt rsa_padding_mode:pss \
|
||||||
|
-keyopt rsa_pss_saltlen:digest \
|
||||||
|
-signer cert.crt \
|
||||||
|
-in "$1" \
|
||||||
|
-out -
|
54
release/dist/qnap/pkgs.go
vendored
54
release/dist/qnap/pkgs.go
vendored
@ -27,8 +27,11 @@ type target struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type signer struct {
|
type signer struct {
|
||||||
privateKeyPath string
|
gcloudCredentialsBase64 string
|
||||||
certificatePath string
|
gcloudProject string
|
||||||
|
gcloudKeyring string
|
||||||
|
keyName string
|
||||||
|
certificateBase64 string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *target) String() string {
|
func (t *target) String() string {
|
||||||
@ -66,7 +69,8 @@ func (t *target) buildQPKG(b *dist.Build, qnapBuilds *qnapBuilds, inner *innerPk
|
|||||||
filename := fmt.Sprintf("Tailscale_%s-%s_%s.qpkg", b.Version.Short, qnapTag, t.arch)
|
filename := fmt.Sprintf("Tailscale_%s-%s_%s.qpkg", b.Version.Short, qnapTag, t.arch)
|
||||||
filePath := filepath.Join(b.Out, filename)
|
filePath := filepath.Join(b.Out, filename)
|
||||||
|
|
||||||
cmd := b.Command(b.Repo, "docker", "run", "--rm",
|
args := []string{"run", "--rm",
|
||||||
|
"--network=host",
|
||||||
"-e", fmt.Sprintf("ARCH=%s", t.arch),
|
"-e", fmt.Sprintf("ARCH=%s", t.arch),
|
||||||
"-e", fmt.Sprintf("TSTAG=%s", b.Version.Short),
|
"-e", fmt.Sprintf("TSTAG=%s", b.Version.Short),
|
||||||
"-e", fmt.Sprintf("QNAPTAG=%s", qnapTag),
|
"-e", fmt.Sprintf("QNAPTAG=%s", qnapTag),
|
||||||
@ -76,10 +80,28 @@ func (t *target) buildQPKG(b *dist.Build, qnapBuilds *qnapBuilds, inner *innerPk
|
|||||||
"-v", fmt.Sprintf("%s:/Tailscale", filepath.Join(qnapBuilds.tmpDir, "files/Tailscale")),
|
"-v", fmt.Sprintf("%s:/Tailscale", filepath.Join(qnapBuilds.tmpDir, "files/Tailscale")),
|
||||||
"-v", fmt.Sprintf("%s:/build-qpkg.sh", filepath.Join(qnapBuilds.tmpDir, "files/scripts/build-qpkg.sh")),
|
"-v", fmt.Sprintf("%s:/build-qpkg.sh", filepath.Join(qnapBuilds.tmpDir, "files/scripts/build-qpkg.sh")),
|
||||||
"-v", fmt.Sprintf("%s:/out", b.Out),
|
"-v", fmt.Sprintf("%s:/out", b.Out),
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.signer != nil {
|
||||||
|
log.Println("Will sign with Google Cloud HSM")
|
||||||
|
args = append(args,
|
||||||
|
"-e", fmt.Sprintf("GCLOUD_CREDENTIALS_BASE64=%s", t.signer.gcloudCredentialsBase64),
|
||||||
|
"-e", fmt.Sprintf("GCLOUD_PROJECT=%s", t.signer.gcloudProject),
|
||||||
|
"-e", fmt.Sprintf("GCLOUD_KEYRING=%s", t.signer.gcloudKeyring),
|
||||||
|
"-e", fmt.Sprintf("QNAP_SIGNING_KEY_NAME=%s", t.signer.keyName),
|
||||||
|
"-e", fmt.Sprintf("QNAP_SIGNING_CERT_BASE64=%s", t.signer.certificateBase64),
|
||||||
|
"-e", fmt.Sprintf("QNAP_SIGNING_SCRIPT=%s", "/sign-qpkg.sh"),
|
||||||
|
"-v", fmt.Sprintf("%s:/sign-qpkg.sh", filepath.Join(qnapBuilds.tmpDir, "files/scripts/sign-qpkg.sh")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args,
|
||||||
"build.tailscale.io/qdk:latest",
|
"build.tailscale.io/qdk:latest",
|
||||||
"/build-qpkg.sh",
|
"/build-qpkg.sh",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cmd := b.Command(b.Repo, "docker", args...)
|
||||||
|
|
||||||
// dist.Build runs target builds in parallel goroutines by default.
|
// dist.Build runs target builds in parallel goroutines by default.
|
||||||
// For QNAP, this is an issue because the underlaying qbuild builder will
|
// For QNAP, this is an issue because the underlaying qbuild builder will
|
||||||
// create tmp directories in the shared docker image that end up conflicting
|
// create tmp directories in the shared docker image that end up conflicting
|
||||||
@ -176,32 +198,6 @@ func newQNAPBuilds(b *dist.Build, signer *signer) (*qnapBuilds, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if signer != nil {
|
|
||||||
log.Print("Setting up qnap signing files")
|
|
||||||
|
|
||||||
key, err := os.ReadFile(signer.privateKeyPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
cert, err := os.ReadFile(signer.certificatePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// QNAP's qbuild command expects key and cert files to be in the root
|
|
||||||
// of the project directory (in our case release/dist/qnap/Tailscale).
|
|
||||||
// So here, we copy the key and cert over to the project folder for the
|
|
||||||
// duration of qnap package building and then delete them on close.
|
|
||||||
|
|
||||||
keyPath := filepath.Join(m.tmpDir, "files/Tailscale/private_key")
|
|
||||||
if err := os.WriteFile(keyPath, key, 0400); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
certPath := filepath.Join(m.tmpDir, "files/Tailscale/certificate")
|
|
||||||
if err := os.WriteFile(certPath, cert, 0400); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
27
release/dist/qnap/targets.go
vendored
27
release/dist/qnap/targets.go
vendored
@ -3,16 +3,31 @@
|
|||||||
|
|
||||||
package qnap
|
package qnap
|
||||||
|
|
||||||
import "tailscale.com/release/dist"
|
import (
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"tailscale.com/release/dist"
|
||||||
|
)
|
||||||
|
|
||||||
// Targets defines the dist.Targets for QNAP devices.
|
// Targets defines the dist.Targets for QNAP devices.
|
||||||
//
|
//
|
||||||
// If privateKeyPath and certificatePath are both provided non-empty,
|
// If all parameters are provided non-empty, then the build will be signed using
|
||||||
// these targets will be signed for QNAP app store release with built.
|
// a Google Cloud hosted key.
|
||||||
func Targets(privateKeyPath, certificatePath string) []dist.Target {
|
//
|
||||||
|
// gcloudCredentialsBase64 is the JSON credential for connecting to Google Cloud, base64 encoded.
|
||||||
|
// gcloudKeyring is the full path to the Google Cloud keyring containing the signing key.
|
||||||
|
// keyName is the name of the key.
|
||||||
|
// certificateBase64 is the PEM certificate to use in the signature, base64 encoded.
|
||||||
|
func Targets(gcloudCredentialsBase64, gcloudProject, gcloudKeyring, keyName, certificateBase64 string) []dist.Target {
|
||||||
var signerInfo *signer
|
var signerInfo *signer
|
||||||
if privateKeyPath != "" && certificatePath != "" {
|
if !slices.Contains([]string{gcloudCredentialsBase64, gcloudProject, gcloudKeyring, keyName, certificateBase64}, "") {
|
||||||
signerInfo = &signer{privateKeyPath, certificatePath}
|
signerInfo = &signer{
|
||||||
|
gcloudCredentialsBase64: gcloudCredentialsBase64,
|
||||||
|
gcloudProject: gcloudProject,
|
||||||
|
gcloudKeyring: gcloudKeyring,
|
||||||
|
keyName: keyName,
|
||||||
|
certificateBase64: certificateBase64,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return []dist.Target{
|
return []dist.Target{
|
||||||
&target{
|
&target{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user