Skip to content

Commit

Permalink
Add interactive flow to credential cache. (#211)
Browse files Browse the repository at this point in the history
This allows for the cache socket to be passed through an SSH client to
allow for remote sessions to use the cache to fetch credentials with the
interactive flow.

Signed-off-by: Billy Lynch <billy@chainguard.dev>

Signed-off-by: Billy Lynch <billy@chainguard.dev>
  • Loading branch information
wlynch authored Jan 12, 2023
1 parent 15447fe commit 98ef482
Show file tree
Hide file tree
Showing 10 changed files with 339 additions and 190 deletions.
40 changes: 39 additions & 1 deletion cmd/gitsign-credential-cache/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ you, until the signing certificate expires, typically in ten minutes.

## Usage

```
```sh
$ gitsign-credential-cache &
$ export GITSIGN_CREDENTIAL_CACHE="$HOME/.cache/.sigstore/gitsign/cache.sock"
$ git commit ...
Expand All @@ -42,3 +42,41 @@ Note: The cache directory will change depending on your OS - the socket file
that is used is output by `gitsign-credential-cache` when it is spawned. See
[os.UserCacheDir](https://pkg.go.dev/os#UserCacheDir) for details on how the
cache directory is selected.

### Forwarding cache over SSH

(Requires gitsign >= v0.5)

The credential cache socket can be forwarded over SSH using `RemoteForward`:

```sh
[local] $ ssh -R /home/wlynch/.sigstore/cache.sock:${GITSIGN_CREDENTIAL_CACHE} <host>
[remote] $ export GITSIGN_CREDENTIAL_CACHE="/home/wlynch/.sigstore/cache.sock"
[remote] $ git commit ...
```

(format is `-R <remote path>:<local path>`)

or in `~/.ssh/config`:

```
Host amazon
RemoteForward /home/wlynch/.sigstore/cache.sock /Users/wlynch/Library/Caches/.sigstore/gitsign/cache.sock
```

where `/home/wlynch/.sigstore/cache.sock` is the location of the socket path on
the remote host (this can be changed, so long as the environment variable is
also updated to match).

#### Common issues

> Warning: remote port forwarding failed for listen path
- The socket directory must exist on the remote, else the socket will fail to
mount.
- We recommend setting `StreamLocalBindUnlink yes` on the remote
`/etc/ssh/sshd_config` to allow for sockets to be overwritten on the same path
for new connections - SSH does not cleanup sockets automatically on exit and
the socket forwarding will fail if a file already exists on the remote path
(see [thread](https://marc.info/?l=openssh-unix-dev&m=151998074424424&w=2) for
more discussion).
4 changes: 2 additions & 2 deletions cmd/gitsign-credential-cache/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import (

"github.com/pborman/getopt/v2"

"github.com/sigstore/gitsign/internal/cache"
"github.com/sigstore/gitsign/internal/cache/service"
"github.com/sigstore/gitsign/pkg/version"
)

Expand Down Expand Up @@ -68,7 +68,7 @@ func main() {
log.Fatalf("error opening socket: %v", err)
}
srv := rpc.NewServer()
if err := srv.Register(cache.NewService()); err != nil {
if err := srv.Register(service.NewService()); err != nil {
log.Fatalf("error registering RPC service: %v", err)
}
for {
Expand Down
33 changes: 33 additions & 0 deletions internal/cache/api/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright 2023 The Sigstore Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package api

import "github.com/sigstore/gitsign/internal/config"

type Credential struct {
PrivateKey []byte
Cert []byte
Chain []byte
}

type StoreCredentialRequest struct {
ID string
Credential *Credential
}

type GetCredentialRequest struct {
ID string
Config *config.Config
}
30 changes: 18 additions & 12 deletions internal/cache/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package cache
package cache_test

import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"fmt"
"net"
"net/rpc"
"os"
Expand All @@ -27,6 +28,9 @@ import (

"github.com/github/smimesign/fakeca"
"github.com/google/go-cmp/cmp"
"github.com/sigstore/gitsign/internal/cache"
"github.com/sigstore/gitsign/internal/cache/api"
"github.com/sigstore/gitsign/internal/cache/service"
"github.com/sigstore/sigstore/pkg/cryptoutils"
)

Expand All @@ -39,7 +43,7 @@ func TestCache(t *testing.T) {
t.Fatal(err)
}
srv := rpc.NewServer()
srv.Register(NewService())
srv.Register(service.NewService())
go func() {
for {
srv.Accept(l)
Expand All @@ -49,12 +53,12 @@ func TestCache(t *testing.T) {
rpcClient, _ := rpc.Dial("unix", path)
defer rpcClient.Close()
ca := fakeca.New()
client := &Client{
client := &cache.Client{
Client: rpcClient,
Roots: ca.ChainPool(),
}

if _, err := client.GetSignerVerifier(ctx); err == nil {
if _, _, _, err := client.GetCredentials(ctx, nil); err == nil {
t.Fatal("GetSignerVerifier: expected err, got not")
}

Expand All @@ -65,14 +69,16 @@ func TestCache(t *testing.T) {
t.Fatalf("StoreCert: %v", err)
}

id, _ := os.Getwd()
cred := new(Credential)
if err := client.Client.Call("Service.GetCredential", &GetCredentialRequest{ID: id}, cred); err != nil {
host, _ := os.Hostname()
wd, _ := os.Getwd()
id := fmt.Sprintf("%s@%s", host, wd)
cred := new(api.Credential)
if err := client.Client.Call("Service.GetCredential", &api.GetCredentialRequest{ID: id}, cred); err != nil {
t.Fatal(err)
}

privPEM, _ := cryptoutils.MarshalPrivateKeyToPEM(priv)
want := &Credential{
want := &api.Credential{
PrivateKey: privPEM,
Cert: certPEM,
}
Expand All @@ -81,14 +87,14 @@ func TestCache(t *testing.T) {
t.Error(diff)
}

got, err := client.GetSignerVerifier(ctx)
gotPriv, gotCert, _, err := client.GetCredentials(ctx, nil)
if err != nil {
t.Fatal(err)
}
if got == nil {
t.Fatal("SignerVerifier was nil")
if !priv.Equal(gotPriv) {
t.Fatal("private key did not match")
}
if ok := cmp.Equal(certPEM, got.Cert); !ok {
if ok := cmp.Equal(certPEM, gotCert); !ok {
t.Error("stored cert does not match")
}
}
67 changes: 33 additions & 34 deletions internal/cache/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ import (
"os"
"time"

"github.com/sigstore/gitsign/internal/signerverifier"
"github.com/sigstore/gitsign/internal/cache/api"
"github.com/sigstore/gitsign/internal/config"
"github.com/sigstore/sigstore/pkg/cryptoutils"
"github.com/sigstore/sigstore/pkg/signature"
)

type Client struct {
Expand All @@ -34,33 +34,28 @@ type Client struct {
Intermediates *x509.CertPool
}

func (c *Client) GetSignerVerifier(ctx context.Context) (*signerverifier.CertSignerVerifier, error) {
id, err := os.Getwd()
func (c *Client) GetCredentials(ctx context.Context, cfg *config.Config) (crypto.PrivateKey, []byte, []byte, error) {
id, err := id()
if err != nil {
return nil, err
return nil, nil, nil, fmt.Errorf("error getting credential ID: %w", err)
}

resp := new(Credential)
if err := c.Client.Call("Service.GetCredential", GetCredentialRequest{
ID: id,
resp := new(api.Credential)
if err := c.Client.Call("Service.GetCredential", api.GetCredentialRequest{
ID: id,
Config: cfg,
}, resp); err != nil {
return nil, err
return nil, nil, nil, err
}

privateKey, err := cryptoutils.UnmarshalPEMToPrivateKey(resp.PrivateKey, cryptoutils.SkipPassword)
if err != nil {
return nil, fmt.Errorf("error unmarshalling private key: %w", err)
}

sv, err := signature.LoadSignerVerifier(privateKey, crypto.SHA256)
if err != nil {
return nil, fmt.Errorf("error creating SignerVerifier: %w", err)
return nil, nil, nil, fmt.Errorf("error unmarshalling private key: %w", err)
}

// Check that the cert is in fact still valid.
certs, err := cryptoutils.UnmarshalCertificatesFromPEM(resp.Cert)
if err != nil {
return nil, fmt.Errorf("error unmarshalling cert: %w", err)
return nil, nil, nil, fmt.Errorf("error unmarshalling cert: %w", err)
}
// There should really only be 1 cert, but check them all anyway.
for _, cert := range certs {
Expand All @@ -72,42 +67,46 @@ func (c *Client) GetSignerVerifier(ctx context.Context) (*signerverifier.CertSig
// Just make sure it's not about to expire.
CurrentTime: time.Now().Add(30 * time.Second),
}); err != nil {
return nil, fmt.Errorf("stored cert no longer valid: %w", err)
return nil, nil, nil, fmt.Errorf("stored cert no longer valid: %w", err)
}
}

return &signerverifier.CertSignerVerifier{
SignerVerifier: sv,
Cert: resp.Cert,
Chain: resp.Chain,
}, nil
}

type PrivateKey interface {
crypto.PrivateKey
Public() crypto.PublicKey
return privateKey, resp.Cert, resp.Chain, nil
}

func (c *Client) StoreCert(ctx context.Context, priv PrivateKey, cert, chain []byte) error {
id, err := os.Getwd()
func (c *Client) StoreCert(ctx context.Context, priv crypto.PrivateKey, cert, chain []byte) error {
id, err := id()
if err != nil {
return err
return fmt.Errorf("error getting credential ID: %w", err)
}
privPEM, err := cryptoutils.MarshalPrivateKeyToPEM(priv)
if err != nil {
return err
}

if err := c.Client.Call("Service.StoreCredential", StoreCredentialRequest{
if err := c.Client.Call("Service.StoreCredential", api.StoreCredentialRequest{
ID: id,
Credential: &Credential{
Credential: &api.Credential{
PrivateKey: privPEM,
Cert: cert,
Chain: chain,
},
}, new(Credential)); err != nil {
}, new(api.Credential)); err != nil {
return err
}

return err
}

func id() (string, error) {
// Prefix host name in case cache socket is being shared over a SSH session.
host, err := os.Hostname()
if err != nil {
return "", fmt.Errorf("error getting hostname: %w", err)
}
wd, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("error getting working directory: %w", err)
}
return fmt.Sprintf("%s@%s", host, wd), nil
}
76 changes: 0 additions & 76 deletions internal/cache/service.go

This file was deleted.

Loading

0 comments on commit 98ef482

Please sign in to comment.