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

Overhaul the notion of a bespoke FIPS-140 mode #44

Open
wants to merge 1 commit into
base: go-1.24
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
47 changes: 46 additions & 1 deletion .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,36 @@ builds:
ldflags:
- -s -w -X github.com/rstudio/rskey/cmd.Version={{ .Version }}
mod_timestamp: '{{ .CommitTimestamp }}'
- id: rskey-fips
env:
- CGO_ENABLED=0
- GOFIPS140=latest
flags:
- -trimpath
ldflags:
- -s -w -X github.com/rstudio/rskey/cmd.Version={{ .Version }}
mod_timestamp: "{{ .CommitTimestamp }}"
targets:
- linux_amd64
archives:
- files:
- builds:
- rskey
files:
- LICENSE
- README.md
- NOTICE.md
format_overrides:
- goos: windows
formats:
- zip
- id: fips
builds:
- rskey-fips
files:
- LICENSE
- README.md
- NOTICE.md
name_template: "{{ .ProjectName }}-fips_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
blobs:
- provider: s3
bucket: rstudio-platform-public-artifacts
Expand Down Expand Up @@ -75,3 +96,27 @@ nfpms:
dst: /usr/share/doc/rskey/README.md
- src: NOTICE.md
dst: /usr/share/doc/rskey/NOTICE.md
- id: fips
package_name: rskey-fips
builds:
- rskey-fips
formats:
- deb
- rpm
conflicts:
- rskey
replaces:
- rskey
section: devel
maintainer: "Posit Software, PBC <[email protected]>"
description: |
A command-line tool that generates secret keys interoperable with the format
used by Posit's Workbench, Connect, and Package Manager products. This is
the FIPS-compliant variant.
contents:
- src: LICENSE
dst: /usr/share/doc/rskey/LICENSE
- src: README.md
dst: /usr/share/doc/rskey/README.md
- src: NOTICE.md
dst: /usr/share/doc/rskey/NOTICE.md
9 changes: 7 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@ ADDLICENSE = go tool github.com/google/addlicense
ADDLICENSE_ARGS = -v -s=only -l=apache -c "Posit Software, PBC" -ignore 'coverage*' -ignore '.github/**' -ignore '.goreleaser.yaml'
NOTICETOOL = go tool go.elastic.co/go-licence-detector

all: rskey
all: rskey rskey-fips

.PHONY: rskey
rskey:
CGO_ENABLED=0 go build -ldflags="$(GO_LDFLAGS)" $(GO_BUILD_ARGS) -o $@ ./$<

.PHONY: rskey-fips
rskey-fips:
CGO_ENABLED=$(CGO_ENABLED) GOFIPS140=latest go build \
-ldflags="$(GO_LDFLAGS)" $(GO_BUILD_ARGS) -o $@ ./$<

.PHONY: static-build
static-build: rskey
ldd $< 2>&1 | grep 'not a dynamic executable'
Expand All @@ -25,7 +30,7 @@ check: fmt vet
test:
go test ./... $(GO_BUILD_ARGS) -coverprofile coverage.out
go tool cover -html=coverage.out -o coverage.html
go test ./... $(GO_BUILD_ARGS) -tags "fips" -coverprofile coverage-fips.out
GOFIPS140=latest go test ./... $(GO_BUILD_ARGS) -coverprofile coverage-fips.out
go tool cover -html=coverage-fips.out -o coverage-fips.html

.PHONY: fmt
Expand Down
20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,22 @@ No local license keys are required, either.
Binary releases for Windows, macOS, and Linux are available [on
GitHub](https://github.com/rstudio/rskey/releases).

We also distribute special `rskey-fips` builds for FIPS 140-3 compliance. These
are compiled against the [Go Cryptographic
Module](https://go.dev/doc/security/fips140).

If you have a local Go toolchain you can also install via `go install`:

``` shell
$ go install github.com/rstudio/rskey@latest
```

or, for a FIPS-compliant version:

``` shell
$ GOFIPS140=v1.0.0 go install github.com/rstudio/rskey@latest
```

Binary releases are signed with [Sigstore](https://www.sigstore.dev/). You can
verify these signatures with their `cosign` tool, for example:

Expand Down Expand Up @@ -85,7 +95,7 @@ Package Manager [version 2024.04.0 and
later](https://docs.posit.co/rspm/news/package-manager/#posit-package-manager-2024040)
support an alternative encryption algorithm, AES-256-GCM. This algorithm is an
Approved Security Function under [Federal Information Processing Standard
140](https://csrc.nist.gov/publications/detail/fips/140/3/final) (FIPS), unlike
140-3](https://csrc.nist.gov/publications/detail/fips/140/3/final) (FIPS), unlike
the default.

If you prefer to encrypt secrets using this algorithm and are using this version
Expand All @@ -99,6 +109,9 @@ $ rskey encrypt -f connect.key --mode=fips
`rskey decrypt` does not require this flag because the algorithm in use can be
determined from the encrypted output.

When using the special `rskey-fips` builds, FIPS mode is the default, and
attempts to decrypt data encryped with a non-FIPS-140-3 algorithm will fail.

### Workbench

Secret keys for Workbench are [traditionally generated by the `uuid`
Expand Down Expand Up @@ -128,8 +141,9 @@ $ rskey encrypt --mode=workbench -f uuid.key
algorithm](https://docs.posit.co/connect/news/#rstudio-connect-2022.03.0),
AES-256-GCM. This algorithm is an Approved Security Function under [Federal
Information Processing Standard
140](https://csrc.nist.gov/publications/detail/fips/140/3/final), and can be
used by passing `--mode=fips` to the `rskey encrypt` command.
140-3](https://csrc.nist.gov/publications/detail/fips/140/3/final), and can be
used by passing `--mode=fips` to the `rskey encrypt` command, or by using the
special `rskey-fips` builds.

* Package Manager version 2024.04.0 and later [supports an identical
setting](https://docs.posit.co/rspm/news/package-manager/#posit-package-manager-2024040).
Expand Down
7 changes: 7 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package cmd

import (
"crypto/fips140"
"os"

"github.com/spf13/cobra"
Expand All @@ -26,3 +27,9 @@ func Execute() {
os.Exit(1)
}
}

func init() {
if fips140.Enabled() {
rootCmd.Version = rootCmd.Version + " (fips)"
}
}
20 changes: 0 additions & 20 deletions crypt/fips.go

This file was deleted.

13 changes: 0 additions & 13 deletions crypt/fips_test.go

This file was deleted.

49 changes: 31 additions & 18 deletions crypt/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@

// Package crypt implements the secret key-based encryption and decryption
// scheme used by Posit's Connect and Package Manager products.
//
// When fips140.Enabled() is true, decrypting data that uses non-compliant
// algorithms instead returns an error, and all encryption uses AES-256-GCM by
// default.
package crypt

import (
"crypto/fips140"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
Expand Down Expand Up @@ -110,31 +115,24 @@ func (k *Key) Encrypt(s string) (string, error) {
return k.EncryptBytes([]byte(s))
}

// EncryptBytes produces base64-encoded cipher text for the given bytes and key,
// or an error if one cannot be created.
// EncryptBytes produces base64-encoded cipher text for the given bytes and key.
// It never returns an error.
func (k *Key) EncryptBytes(bytes []byte) (string, error) {
var output []byte
if FIPSMode {
if fipsMode {
output := k.encryptAES(bytes)
return base64.StdEncoding.EncodeToString(output), nil
}
output, err := k.encryptSecretbox(bytes)
if err != nil {
return "", err
}
output := k.encryptSecretbox(bytes)
return base64.StdEncoding.EncodeToString(output), nil
}

// encryptVersioned produces a base64-encoded cipher text with an embedded
// version for the given payload and key, or an error if one cannot be created.
// This emulates the format used by some implementations.
func (k *Key) encryptVersioned(s string) (string, error) {
output, err := k.encryptSecretbox([]byte(s))
if err != nil {
return "", err
}
// version for the given payload and key. This emulates the format used by some
// implementations.
func (k *Key) encryptVersioned(s string) string {
output := k.encryptSecretbox([]byte(s))
output = append([]byte{1}, output...)
return base64.StdEncoding.EncodeToString(output), nil
return base64.StdEncoding.EncodeToString(output)
}

// Decrypt takes base64-encoded cipher text encrypted with the given key and
Expand All @@ -158,15 +156,21 @@ func (k *Key) DecryptBytes(s string) ([]byte, error) {
// handle the (unlikely but possible) case where a versionless payload
// *just happens* to start with a valid version byte, we must also try
// the fallback on error.
//
// Unless we're in FIPS mode, in which case only the AES version is
// available.
if fipsMode && buf[0] != byte(2) {
return []byte{}, ErrFIPS
}
switch buf[0] {
case byte(1):
str, err := k.decryptSecretbox(buf[1:])
if err == nil || FIPSMode {
if err == nil || fipsMode {
return str, err
}
case byte(2):
str, err := k.decryptAES(buf)
if err == nil || FIPSMode {
if err == nil || fipsMode {
return str, err
}
}
Expand All @@ -191,3 +195,12 @@ func rotate(data []byte) []byte {
}
return newData
}

// Deprecated. Use fips140.Enabled() instead.
const FIPSMode = false

var fipsMode = false

func init() {
fipsMode = fips140.Enabled()
}
51 changes: 45 additions & 6 deletions crypt/key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
package crypt

import (
"crypto/fips140"
"encoding/base64"
"encoding/hex"
"fmt"
"strings"
Expand Down Expand Up @@ -110,7 +112,7 @@ func (s *KeySuite) TestEncryption(c *check.C) {

// These payloads do not have a the FIPS version prefix, so they will
// generate a different error in FIPS mode.
if !FIPSMode {
if !fips140.Enabled() {
_, err = key.Decrypt("ycKfTfYlVaOnsypb")
c.Check(err, check.Equals, ErrPayLoadTooShort)

Expand All @@ -133,7 +135,7 @@ func (s *KeySuite) TestEncryption(c *check.C) {
}

func (s *KeySuite) TestVersionedEncryption(c *check.C) {
if FIPSMode {
if fips140.Enabled() {
c.ExpectFailure("NaCl encryption will not work under FIPS")
}
key, _ := NewKey()
Expand All @@ -147,16 +149,14 @@ func (s *KeySuite) TestVersionedEncryption(c *check.C) {
c.Check(err, check.Equals, ErrFailedToDecrypt)

// Roundtrip encryption test.
cipher, err := key.encryptVersioned("some secret")
c.Check(err, check.IsNil)
cipher := key.encryptVersioned("some secret")
c.Check(cipher, check.Not(check.Equals), "some secret") // Just checking.
text, err := key.Decrypt(cipher)
c.Check(err, check.IsNil)
c.Check(text, check.Equals, "some secret")

// Check that nonces actually work.
dupCipher, err := key.encryptVersioned("some secret")
c.Check(err, check.IsNil)
dupCipher := key.encryptVersioned("some secret")
c.Check(dupCipher, check.Not(check.Equals), cipher)
}

Expand Down Expand Up @@ -201,6 +201,45 @@ func (s *KeySuite) TestFIPSEncryption(c *check.C) {
dupCipher, err := key.EncryptFIPS("some secret")
c.Check(err, check.IsNil)
c.Check(dupCipher, check.Not(check.Equals), cipher)

// Check that explicitly setting FIPS mode switches the algorithm.
originalMode := fipsMode
fipsMode = true
defer func() { fipsMode = originalMode }()
cipher, err = key.Encrypt("some secret")
c.Check(err, check.IsNil)
buf, _ := base64.StdEncoding.DecodeString(cipher)
c.Check(buf[0], check.Equals, byte(2))
}

func (s *KeySuite) TestFIPSMode(c *check.C) {
originalMode := fipsMode
fipsMode = true
defer func() { fipsMode = originalMode }()
key, _ := NewKey()

// Check that explicitly setting FIPS mode ensures we use AES.
cipher, err := key.Encrypt("some secret")
c.Check(err, check.IsNil)
buf, _ := base64.StdEncoding.DecodeString(cipher)
c.Check(buf[0], check.Equals, byte(2))

// Check that non-AES ciphers cannot be decrypted in this mode.
cipher = key.encryptVersioned("some secret")
_, err = key.Decrypt(cipher)
c.Check(err, check.Equals, ErrFIPS)
}

func (s *KeySuite) TestFIPSCompliance(c *check.C) {
// Verify that non-FIPS algorithms can't be called through the public
// API in this mode.
if !fips140.Enabled() {
c.Skip("skipping FIPS 140-3 compliance tests")
}
key, _ := NewKey()
cipher := key.encryptVersioned("some secret")
_, err := key.Decrypt(cipher)
c.Check(err, check.Equals, ErrFIPS)
}

func (s *KeySuite) TestFingerprint(c *check.C) {
Expand Down
Loading
Loading