mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 13:05:22 +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:
		 Percy Wegmann
					Percy Wegmann
				
			
				
					committed by
					
						 Percy Wegmann
						Percy Wegmann
					
				
			
			
				
	
			
			
			 Percy Wegmann
						Percy Wegmann
					
				
			
						parent
						
							0c78f081a4
						
					
				
				
					commit
					26f31f73f4
				
			
							
								
								
									
										25
									
								
								cmd/dist/dist.go
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										25
									
								
								cmd/dist/dist.go
									
									
									
									
										vendored
									
									
								
							| @@ -5,11 +5,13 @@ | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"cmp" | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"flag" | ||||
| 	"log" | ||||
| 	"os" | ||||
| 	"slices" | ||||
| 
 | ||||
| 	"tailscale.com/release/dist" | ||||
| 	"tailscale.com/release/dist/cli" | ||||
| @@ -19,9 +21,12 @@ import ( | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	synologyPackageCenter bool | ||||
| 	qnapPrivateKeyPath    string | ||||
| 	qnapCertificatePath   string | ||||
| 	synologyPackageCenter   bool | ||||
| 	gcloudCredentialsBase64 string | ||||
| 	gcloudProject           string | ||||
| 	gcloudKeyring           string | ||||
| 	qnapKeyName             string | ||||
| 	qnapCertificateBase64   string | ||||
| ) | ||||
| 
 | ||||
| func getTargets() ([]dist.Target, error) { | ||||
| @@ -42,10 +47,11 @@ func getTargets() ([]dist.Target, error) { | ||||
| 	// To build for package center, run | ||||
| 	// ./tool/go run ./cmd/dist build --synology-package-center synology | ||||
| 	ret = append(ret, synology.Targets(synologyPackageCenter, nil)...) | ||||
| 	if (qnapPrivateKeyPath == "") != (qnapCertificatePath == "") { | ||||
| 		return nil, errors.New("both --qnap-private-key-path and --qnap-certificate-path must be set") | ||||
| 	qnapSigningArgs := []string{gcloudCredentialsBase64, gcloudProject, gcloudKeyring, qnapKeyName, qnapCertificateBase64} | ||||
| 	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 | ||||
| } | ||||
| 
 | ||||
| @@ -54,8 +60,11 @@ func main() { | ||||
| 	for _, subcmd := range cmd.Subcommands { | ||||
| 		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.StringVar(&qnapPrivateKeyPath, "qnap-private-key-path", "", "sign qnap packages with given key (must also provide --qnap-certificate-path)") | ||||
| 			subcmd.FlagSet.StringVar(&qnapCertificatePath, "qnap-certificate-path", "", "sign qnap packages with given certificate (must also provide --qnap-private-key-path)") | ||||
| 			subcmd.FlagSet.StringVar(&gcloudCredentialsBase64, "gcloud-credentials", "", "base64 encoded GCP credentials (used when signing QNAP builds)") | ||||
| 			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") | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|   | ||||
							
								
								
									
										20
									
								
								release/dist/qnap/files/scripts/Dockerfile.qpkg
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										20
									
								
								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 && \ | ||||
|     apt-get install -y --no-install-recommends \ | ||||
|     git-core \ | ||||
|     ca-certificates | ||||
| RUN git clone https://github.com/qnap-dev/QDK.git | ||||
|     ca-certificates \ | ||||
|     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 | ||||
| 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 { | ||||
| 	privateKeyPath  string | ||||
| 	certificatePath string | ||||
| 	gcloudCredentialsBase64 string | ||||
| 	gcloudProject           string | ||||
| 	gcloudKeyring           string | ||||
| 	keyName                 string | ||||
| 	certificateBase64       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) | ||||
| 	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("TSTAG=%s", b.Version.Short), | ||||
| 		"-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:/build-qpkg.sh", filepath.Join(qnapBuilds.tmpDir, "files/scripts/build-qpkg.sh")), | ||||
| 		"-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-qpkg.sh", | ||||
| 	) | ||||
| 
 | ||||
| 	cmd := b.Command(b.Repo, "docker", args...) | ||||
| 
 | ||||
| 	// dist.Build runs target builds in parallel goroutines by default. | ||||
| 	// For QNAP, this is an issue because the underlaying qbuild builder will | ||||
| 	// 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 | ||||
| 	} | ||||
| 
 | ||||
| 	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 | ||||
| } | ||||
| 
 | ||||
|   | ||||
							
								
								
									
										27
									
								
								release/dist/qnap/targets.go
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										27
									
								
								release/dist/qnap/targets.go
									
									
									
									
										vendored
									
									
								
							| @@ -3,16 +3,31 @@ | ||||
| 
 | ||||
| package qnap | ||||
| 
 | ||||
| import "tailscale.com/release/dist" | ||||
| import ( | ||||
| 	"slices" | ||||
| 
 | ||||
| 	"tailscale.com/release/dist" | ||||
| ) | ||||
| 
 | ||||
| // Targets defines the dist.Targets for QNAP devices. | ||||
| // | ||||
| // If privateKeyPath and certificatePath are both provided non-empty, | ||||
| // these targets will be signed for QNAP app store release with built. | ||||
| func Targets(privateKeyPath, certificatePath string) []dist.Target { | ||||
| // If all parameters are provided non-empty, then the build will be signed using | ||||
| // a Google Cloud hosted key. | ||||
| // | ||||
| // 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 | ||||
| 	if privateKeyPath != "" && certificatePath != "" { | ||||
| 		signerInfo = &signer{privateKeyPath, certificatePath} | ||||
| 	if !slices.Contains([]string{gcloudCredentialsBase64, gcloudProject, gcloudKeyring, keyName, certificateBase64}, "") { | ||||
| 		signerInfo = &signer{ | ||||
| 			gcloudCredentialsBase64: gcloudCredentialsBase64, | ||||
| 			gcloudProject:           gcloudProject, | ||||
| 			gcloudKeyring:           gcloudKeyring, | ||||
| 			keyName:                 keyName, | ||||
| 			certificateBase64:       certificateBase64, | ||||
| 		} | ||||
| 	} | ||||
| 	return []dist.Target{ | ||||
| 		&target{ | ||||
|   | ||||
		Reference in New Issue
	
	Block a user