Skip to content

Commit 4158540

Browse files
committed
Code signing: sign Linux executables with GPG
Use GnuPG to sign the Linux executables in our release. The signatures are detached in .asc files, one signature per executable.
1 parent 46bf149 commit 4158540

File tree

5 files changed

+256
-4
lines changed

5 files changed

+256
-4
lines changed

dist/certificates/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ documentation](../../docs/CODE_SIGNING.md).
1111
* `quick-lint-js.cer`: Self-signed certificate (public key). Use this file to
1212
verify signatures of quick-lint-js on macOS.
1313
* SHA1: dc4f675b74b3a86c1f59fbdac17538b7d996db99
14+
* `quick-lint-js.gpg.key`: GPG key (public key). Use this file to verify GPG
15+
signatures of quick-lint-js.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
pub rsa3072 2021-01-21 [SC] [expires: 2023-01-21]
2+
0327 DE8F 9CEF 4998 51D1 9F6E D20B A9DC CF0E 9D20
3+
uid [ultimate] Matthew "strager" Glazar (quick-lint-js signing key) <[email protected]>
4+
sig!3 D20BA9DCCF0E9D20 2021-01-21 Matthew "strager" Glazar (quick-lint-js signing key) <[email protected]>
5+
sub rsa3072 2021-01-21 [E] [expires: 2023-01-21]
6+
sig! D20BA9DCCF0E9D20 2021-01-21 Matthew "strager" Glazar (quick-lint-js signing key) <[email protected]>
7+
8+
-----BEGIN PGP PUBLIC KEY BLOCK-----
9+
10+
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
47+
wK1pk2CDI9ALK6lAoAcA/gNux5w=
48+
=l5TX
49+
-----END PGP PUBLIC KEY BLOCK-----

dist/sign-release.go

Lines changed: 200 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,15 @@ import _ "embed"
2525
//go:embed certificates/quick-lint-js.cer
2626
var AppleCodesignCertificate []byte
2727

28+
//go:embed certificates/quick-lint-js.gpg.key
29+
var QLJSGPGKey []byte
30+
2831
type SigningStuff struct {
2932
AppleCodesignIdentity string // Common Name from the macOS Keychain.
3033
Certificate []byte
3134
CertificateSHA1Hash [20]byte
35+
GPGIdentity string // Fingerprint or email or name.
36+
GPGKey []byte
3237
PrivateKeyPKCS12Path string
3338
}
3439

@@ -39,8 +44,10 @@ func main() {
3944

4045
var signingStuff SigningStuff
4146
signingStuff.Certificate = AppleCodesignCertificate
47+
signingStuff.GPGKey = QLJSGPGKey
4248

4349
flag.StringVar(&signingStuff.AppleCodesignIdentity, "AppleCodesignIdentity", "", "")
50+
flag.StringVar(&signingStuff.GPGIdentity, "GPGIdentity", "", "")
4451
flag.StringVar(&signingStuff.PrivateKeyPKCS12Path, "PrivateKeyPKCS12", "", "")
4552
flag.Parse()
4653
if flag.NArg() != 2 {
@@ -92,10 +99,20 @@ type FileTransformType int
9299
const (
93100
NoTransform FileTransformType = iota
94101
AppleCodesign
102+
GPGSign
95103
MicrosoftOsslsigncode
96104
)
97105

98106
var filesToTransform map[string]map[string]FileTransformType = map[string]map[string]FileTransformType{
107+
"manual/linux-aarch64.tar.gz": map[string]FileTransformType{
108+
"quick-lint-js/bin/quick-lint-js": GPGSign,
109+
},
110+
"manual/linux-armhf.tar.gz": map[string]FileTransformType{
111+
"quick-lint-js/bin/quick-lint-js": GPGSign,
112+
},
113+
"manual/linux.tar.gz": map[string]FileTransformType{
114+
"quick-lint-js/bin/quick-lint-js": GPGSign,
115+
},
99116
"manual/macos-aarch64.tar.gz": map[string]FileTransformType{
100117
"quick-lint-js/bin/quick-lint-js": AppleCodesign,
101118
},
@@ -114,12 +131,18 @@ var filesToTransform map[string]map[string]FileTransformType = map[string]map[st
114131
"npm/quick-lint-js-0.5.0.tgz": map[string]FileTransformType{
115132
"package/darwin-aarch64/bin/quick-lint-js": AppleCodesign,
116133
"package/darwin-x64/bin/quick-lint-js": AppleCodesign,
134+
"package/linux-arm/bin/quick-lint-js": GPGSign,
135+
"package/linux-arm64/bin/quick-lint-js": GPGSign,
136+
"package/linux-x64/bin/quick-lint-js": GPGSign,
117137
"package/win32-arm64/bin/quick-lint-js": MicrosoftOsslsigncode,
118138
"package/win32-x64/bin/quick-lint-js": MicrosoftOsslsigncode,
119139
},
120140
"vscode/quick-lint-js-0.5.0.vsix": map[string]FileTransformType{
121141
"extension/dist/quick-lint-js-vscode-node_darwin-arm64.node": AppleCodesign,
122142
"extension/dist/quick-lint-js-vscode-node_darwin-x64.node": AppleCodesign,
143+
"extension/dist/quick-lint-js-vscode-node_linux-arm.node": GPGSign,
144+
"extension/dist/quick-lint-js-vscode-node_linux-arm64.node": GPGSign,
145+
"extension/dist/quick-lint-js-vscode-node_linux-x64.node": GPGSign,
123146
"extension/dist/quick-lint-js-vscode-node_win32-arm.node": MicrosoftOsslsigncode,
124147
"extension/dist/quick-lint-js-vscode-node_win32-arm64.node": MicrosoftOsslsigncode,
125148
"extension/dist/quick-lint-js-vscode-node_win32-ia32.node": MicrosoftOsslsigncode,
@@ -195,6 +218,14 @@ type FileTransformResult struct {
195218
// If newFile is not nil, Close will delete the file as if by
196219
// os.Remove(newFile.Name())
197220
newFile *os.File
221+
222+
// If siblingFile is not nil, then a new file is created named
223+
// siblingFileName.
224+
//
225+
// If siblingFile is not nil, Close will delete the file as if by
226+
// os.Remove(siblingFile.Name()).
227+
siblingFile *os.File
228+
siblingFileName string
198229
}
199230

200231
func (self *FileTransformResult) UpdateTarHeader(header *tar.Header) error {
@@ -232,6 +263,12 @@ func (self *FileTransformResult) Close() {
232263
os.Remove(self.newFile.Name())
233264
self.newFile = nil
234265
}
266+
267+
if self.siblingFile != nil {
268+
self.siblingFile.Close()
269+
os.Remove(self.siblingFile.Name())
270+
self.siblingFile = nil
271+
}
235272
}
236273

237274
func TransformTarGz(
@@ -252,6 +289,14 @@ func TransformTarGz(
252289
}
253290
return transform, nil
254291

292+
case GPGSign:
293+
log.Printf("signing with GPG: %s:%s\n", sourceFilePath, path)
294+
transform, err := GPGSignTransform(path, file, signingStuff)
295+
if err != nil {
296+
return FileTransformResult{}, err
297+
}
298+
return transform, nil
299+
255300
case MicrosoftOsslsigncode:
256301
log.Printf("signing with osslsigncode: %s:%s\n", sourceFilePath, path)
257302
transform, err := MicrosoftOsslsigncodeTransform(file, signingStuff)
@@ -289,7 +334,16 @@ func TransformTarGzGeneric(
289334
return err
290335
}
291336

292-
transformResult, err := transform(header.Name, tarReader)
337+
var fileContent bytes.Buffer
338+
bytesWritten, err := fileContent.ReadFrom(tarReader)
339+
if err != nil {
340+
return err
341+
}
342+
if bytesWritten != header.Size {
343+
return fmt.Errorf("failed to read entire file")
344+
}
345+
346+
transformResult, err := transform(header.Name, bytes.NewReader(fileContent.Bytes()))
293347
if err != nil {
294348
return err
295349
}
@@ -298,13 +352,35 @@ func TransformTarGzGeneric(
298352
if err := transformResult.UpdateTarHeader(header); err != nil {
299353
return err
300354
}
301-
var file io.Reader = tarReader
355+
var file io.Reader = bytes.NewReader(fileContent.Bytes())
302356
if transformResult.newFile != nil {
303357
file = transformResult.newFile
304358
}
305359
if err := WriteTarEntry(header, file, tarWriter); err != nil {
306360
return err
307361
}
362+
363+
if transformResult.siblingFile != nil {
364+
siblingFileStat, err := transformResult.siblingFile.Stat()
365+
if err != nil {
366+
return err
367+
}
368+
siblingHeader := tar.Header{
369+
Typeflag: tar.TypeReg,
370+
Name: filepath.Join(filepath.Dir(header.Name), transformResult.siblingFileName),
371+
Size: siblingFileStat.Size(),
372+
Mode: header.Mode &^ 0111,
373+
Uid: header.Uid,
374+
Gid: header.Gid,
375+
Uname: header.Uname,
376+
Gname: header.Gname,
377+
ModTime: siblingFileStat.ModTime(),
378+
Format: tar.FormatUSTAR,
379+
}
380+
if err := WriteTarEntry(&siblingHeader, transformResult.siblingFile, tarWriter); err != nil {
381+
return err
382+
}
383+
}
308384
}
309385
return nil
310386
}
@@ -326,6 +402,14 @@ func TransformZip(
326402
}
327403
return transform, nil
328404

405+
case GPGSign:
406+
log.Printf("signing with GPG: %s:%s\n", sourceFile.Name(), path)
407+
transform, err := GPGSignTransform(path, file, signingStuff)
408+
if err != nil {
409+
return FileTransformResult{}, err
410+
}
411+
return transform, nil
412+
329413
case MicrosoftOsslsigncode:
330414
log.Printf("signing with osslsigncode: %s:%s\n", sourceFile.Name(), path)
331415
transform, err := MicrosoftOsslsigncodeTransform(file, signingStuff)
@@ -397,6 +481,17 @@ func TransformZipGeneric(
397481
return err
398482
}
399483
}
484+
485+
if transformResult.siblingFile != nil {
486+
siblingZIPEntryFile, err := destinationZipFile.Create(filepath.Join(filepath.Dir(zipEntry.Name), transformResult.siblingFileName))
487+
if err != nil {
488+
return err
489+
}
490+
_, err = io.Copy(siblingZIPEntryFile, transformResult.siblingFile)
491+
if err != nil {
492+
return err
493+
}
494+
}
400495
}
401496
return nil
402497
}
@@ -479,6 +574,109 @@ func AppleCodesignVerifyFile(filePath string, signingStuff SigningStuff) error {
479574
return nil
480575
}
481576

577+
// originalPath need not be a path to a real file.
578+
func GPGSignTransform(originalPath string, exe io.Reader, signingStuff SigningStuff) (FileTransformResult, error) {
579+
tempDir, err := ioutil.TempDir("", "quick-lint-js-sign-release")
580+
if err != nil {
581+
return FileTransformResult{}, err
582+
}
583+
TempDirs = append(TempDirs, tempDir)
584+
585+
tempFile, err := os.Create(filepath.Join(tempDir, "data"))
586+
if err != nil {
587+
return FileTransformResult{}, err
588+
}
589+
defer os.Remove(tempFile.Name())
590+
_, err = io.Copy(tempFile, exe)
591+
tempFile.Close()
592+
if err != nil {
593+
return FileTransformResult{}, err
594+
}
595+
596+
signatureFilePath, err := GPGSignFile(tempFile.Name(), signingStuff)
597+
if err != nil {
598+
return FileTransformResult{}, err
599+
}
600+
if err := GPGVerifySignature(tempFile.Name(), signatureFilePath, signingStuff); err != nil {
601+
return FileTransformResult{}, err
602+
}
603+
604+
signatureFile, err := os.Open(signatureFilePath)
605+
if err != nil {
606+
return FileTransformResult{}, err
607+
}
608+
609+
return FileTransformResult{
610+
siblingFile: signatureFile,
611+
siblingFileName: filepath.Base(originalPath) + ".asc",
612+
}, nil
613+
}
614+
615+
func GPGSignFile(filePath string, signingStuff SigningStuff) (string, error) {
616+
process := exec.Command(
617+
"gpg",
618+
"--local-user", signingStuff.GPGIdentity,
619+
"--armor",
620+
"--detach-sign",
621+
"--",
622+
filePath,
623+
)
624+
process.Stdout = os.Stdout
625+
process.Stderr = os.Stderr
626+
if err := process.Start(); err != nil {
627+
return "", err
628+
}
629+
if err := process.Wait(); err != nil {
630+
return "", err
631+
}
632+
return filePath + ".asc", nil
633+
}
634+
635+
func GPGVerifySignature(filePath string, signatureFilePath string, signingStuff SigningStuff) error {
636+
// HACK(strager): Use /tmp instead of the default temp dir. macOS'
637+
// default temp dir is so long that it breaks gpg-agent.
638+
tempGPGHome, err := ioutil.TempDir("/tmp", "quick-lint-js-sign-release")
639+
if err != nil {
640+
return err
641+
}
642+
TempDirs = append(TempDirs, tempGPGHome)
643+
644+
var env []string
645+
env = append([]string{}, os.Environ()...)
646+
env = append(env, "GNUPGHOME="+tempGPGHome)
647+
648+
process := exec.Command("gpg", "--import")
649+
keyReader := bytes.NewReader(signingStuff.GPGKey)
650+
process.Stdin = keyReader
651+
process.Stdout = os.Stdout
652+
process.Stderr = os.Stderr
653+
process.Env = env
654+
if err := process.Start(); err != nil {
655+
return err
656+
}
657+
if err := process.Wait(); err != nil {
658+
return err
659+
}
660+
661+
process = exec.Command(
662+
"gpg", "--verify",
663+
"--",
664+
signatureFilePath,
665+
filePath,
666+
)
667+
process.Stdout = os.Stdout
668+
process.Stderr = os.Stderr
669+
process.Env = env
670+
if err := process.Start(); err != nil {
671+
return err
672+
}
673+
if err := process.Wait(); err != nil {
674+
return err
675+
}
676+
677+
return nil
678+
}
679+
482680
func MicrosoftOsslsigncodeTransform(exe io.Reader, signingStuff SigningStuff) (FileTransformResult, error) {
483681
tempDir, err := ioutil.TempDir("", "quick-lint-js-sign-release")
484682
if err != nil {

docs/CODE_SIGNING.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@ In the Keychain Access app, export your code signing certificate (not CA) and
1313
private key as a .p12 file. Call it
1414
`dist/certificates/quick-lint-js-PRIVATE.p12`. **Do not commit this file.**
1515

16+
You will also need a GnuPG key. Create the key, then export the public key to
17+
`dist/certificates/quick-lint-js.gpg.key`.
18+
1619
When you run the `dist/sign-release.go` program, specify
17-
`-AppleCodesignIdentity COMMON_NAME_OF_YOUR_KEY -PrivateKeyPKCS12 dist/certificates/quick-lint-js-PRIVATE.p12`.
20+
`-AppleCodesignIdentity COMMON_NAME_OF_YOUR_KEY -GPGIdentity GPG_KEY_FINGERPRINT -PrivateKeyPKCS12 dist/certificates/quick-lint-js-PRIVATE.p12`.
1821

1922
[macos-create-ca]: https://www.simplified.guide/macos/keychain-ca-code-signing-create
2023
[macos-create-cert]: https://www.simplified.guide/macos/keychain-cert-code-signing-create

docs/RELEASE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ Follow the following steps to release a new version of quick-lint-js:
3333
`rsync -av c.quick-lint-js.com:/var/www/c.quick-lint-js.com/builds/$YOUR_COMMIT_HASH/ builds/`
3434

3535
7. Sign the build artifacts:
36-
`go run dist/sign-releases.go -AppleCodesignIdentity=quick-lint-js -PrivateKeyPKCS12=dist/certificates/quick-lint-js-PRIVATE.p12 builds/ signed-builds/`
36+
`go run dist/sign-releases.go -AppleCodesignIdentity=quick-lint-js -GPGIdentity=0327DE8F9CEF499851D19F6ED20BA9DCCF0E9D20 -PrivateKeyPKCS12=dist/certificates/quick-lint-js-PRIVATE.p12 builds/ signed-builds/`
3737
* **Warning**: This signing command only works on macOS hosts.
3838

3939
8. Upload the signed build artifacts to the artifact server:

0 commit comments

Comments
 (0)