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

Add delegation #611

Merged
merged 10 commits into from
Feb 8, 2023
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
piv-attestation-ca.pem
/keygen
test-root.pem
test-root-attestation.ca
test-root-attestation.ca
*~
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,7 @@ keygen:
.PHONY: tuf
tuf:
@go build -tags=pivkey -o $@ ./cmd/tuf

.PHONY: test
test:
@go test -tags=pivkey ./...
104 changes: 74 additions & 30 deletions cmd/tuf/app/add-delegation.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ package app

import (
"context"
"errors"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"strings"

csignature "github.com/sigstore/cosign/pkg/signature"
pkeys "github.com/sigstore/root-signing/pkg/keys"
prepo "github.com/sigstore/root-signing/pkg/repo"
"github.com/theupdateframework/go-tuf/data"
Expand All @@ -44,28 +44,37 @@ func (f *keysFlag) Set(value string) error {
return nil
}

type DelegationOptions struct {
Directory string
Name string
Path string
Terminating bool
KeyRefs []string
Threshold int
Targets string
}

func AddDelegation() *ffcli.Command {
var (
flagset = flag.NewFlagSet("tuf add-delegation", flag.ExitOnError)
repository = flagset.String("repository", "", "path to the staged repository")
name = flagset.String("name", "", "name of the delegatee")
keys = keysFlag{}
path = flagset.String("path", "", "path for the delegation")
terminating = flagset.Bool("terminating", false, "indicated whether the delegation is terminating")
asraa marked this conversation as resolved.
Show resolved Hide resolved
targets = flagset.String("target-meta", "", "path to a target configuration file")
flagset = flag.NewFlagSet("tuf add-delegation", flag.ExitOnError)
repository = flagset.String("repository", "", "path to the staged repository")
name = flagset.String("name", "", "name of the delegatee")
keys = keysFlag{}
targets = flagset.String("target-meta", "", "path to a target configuration file")
threshold = flagset.Int("threshold", 1, "default delegation signer threshold")
)
flagset.Var(&keys, "key", "key reference for the delegatee")
flagset.Var(&keys, "public-key", "public key reference for the delegatee")
return &ffcli.Command{
Name: "add-delegation",
ShortUsage: "tuf add-delegation a role delegation from the top-level targets",
ShortHelp: "tuf add-delegation a role delegation from the top-level targets",
LongHelp: `tuf add-delegation a role delegation from the top-level targets.
Adds a targets delegation with a name and specified keys. The optional path can also be set,
Adds a targets delegation with a name and specified keys. The optional path can also be set,
but will default to the name if unspecified.

EXAMPLES
# add-delegation repository at ceremony/YYYY-MM-DD
tuf add-delegation -repository ceremony/YYYY-MM-DD -name $NAME -key $KEY_A -key $KEY_B -path $PATH`,
tuf add-delegation -repository ceremony/YYYY-MM-DD -name $NAME -key $KEY_A -key $KEY_B`,
FlagSet: flagset,
Exec: func(ctx context.Context, args []string) error {
if *repository == "" {
Expand All @@ -77,21 +86,38 @@ but will default to the name if unspecified.
if len(keys) == 0 {
return flag.ErrHelp
}
return DelegationCmd(ctx, *repository, *name, *path, *terminating, keys, *targets)
if len(keys) < *threshold {
return flag.ErrHelp
}
opts := DelegationOptions{
Directory: *repository,
Name: *name,
Path: filepath.Join(*name, "*"),
Terminating: true,
KeyRefs: []string(keys),
Threshold: *threshold,
Targets: *targets,
}
return DelegationCmd(ctx, &opts)
},
}
}

func DelegationCmd(ctx context.Context, directory, name, path string, terminating bool, keyRefs keysFlag, targets string) error {
store := tuf.FileSystemStore(directory, nil)
func DelegationCmd(ctx context.Context, opts *DelegationOptions) error {
asraa marked this conversation as resolved.
Show resolved Hide resolved
store := tuf.FileSystemStore(opts.Directory, nil)

if len(opts.KeyRefs) < opts.Threshold {
return fmt.Errorf("configured threshold is %d but only %d keys provided",
opts.Threshold, len(opts.KeyRefs))
}
if opts.Path == "" {
return errors.New("empty path provided")
}

repo, err := tuf.NewRepoIndent(store, "", "\t", "sha512", "sha256")
if err != nil {
return err
}
if path == "" {
path = name
}

// Store signature placeholders
s, err := prepo.GetSignedMeta(store, "targets.json")
Expand All @@ -102,14 +128,18 @@ func DelegationCmd(ctx context.Context, directory, name, path string, terminatin

keys := []*data.PublicKey{}
ids := []string{}
for _, keyRef := range keyRefs {
signer, err := csignature.SignerVerifierFromKeyRef(ctx, keyRef, nil)
for _, keyRef := range opts.KeyRefs {
verifier, err := GetVerifier(ctx, keyRef)
if err != nil {
return err
}

// Construct TUF key.
tufKey, err := pkeys.ConstructTufKey(ctx, signer, DeprecatedEcdsaFormat)
publicKey, err := verifier.PublicKey()
if err != nil {
return err
}
tufKey, err := pkeys.ConstructTufKeyFromPublic(ctx, publicKey, DeprecatedEcdsaFormat)
if err != nil {
return err
}
Expand All @@ -124,19 +154,19 @@ func DelegationCmd(ctx context.Context, directory, name, path string, terminatin
}

if err := repo.AddDelegatedRoleWithExpires("targets", data.DelegatedRole{
Name: name,
Name: opts.Name,
KeyIDs: ids,
Paths: []string{path},
Threshold: 1,
Terminating: terminating,
Paths: []string{opts.Path},
Threshold: opts.Threshold,
Terminating: opts.Terminating,
}, keys, GetExpiration("targets")); err != nil {
// If delegation already added, then we just want to bump version and expiration.
fmt.Fprintln(os.Stdout, "Adding targets delegation: ", err)
}

// Add targets (copy them into the repository and add them to the targets.json)
if targets != "" {
targetCfg, err := os.ReadFile(targets)
if opts.Targets != "" {
targetCfg, err := os.ReadFile(opts.Targets)
if err != nil {
return err
}
Expand All @@ -146,14 +176,28 @@ func DelegationCmd(ctx context.Context, directory, name, path string, terminatin
return err
}

for tt, custom := range meta {
// Remove the targets as requested
for tt := range meta.Del {
err = repo.RemoveTarget(tt)
if err != nil {
return err
}
}

for tt, custom := range meta.Add {
from, err := os.Open(tt)
if err != nil {
return err
}
defer from.Close()
base := filepath.Base(tt)
to, err := os.Create(filepath.Join(directory, "staged", "targets", base))
dir := filepath.Dir(tt)
toDir := filepath.Join(opts.Directory, "staged", "targets", dir)
err = os.MkdirAll(toDir, 0750)
if err != nil {
return err
}
to, err := os.Create(filepath.Join(toDir, base))
if err != nil {
return err
}
Expand All @@ -162,7 +206,7 @@ func DelegationCmd(ctx context.Context, directory, name, path string, terminatin
return err
}
fmt.Fprintln(os.Stderr, "Created target file at ", to.Name())
if err := repo.AddTargetsWithExpiresToPreferredRole([]string{base}, custom, GetExpiration("targets"), name); err != nil {
if err := repo.AddTargetsWithExpiresToPreferredRole([]string{tt}, custom, GetExpiration("targets"), opts.Name); err != nil {
return fmt.Errorf("error adding targets %w", err)
}
}
Expand Down
68 changes: 68 additions & 0 deletions cmd/tuf/app/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//
// 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 app

import (
"context"
"crypto"
"fmt"

"github.com/sigstore/cosign/pkg/cosign/pivkey"
csignature "github.com/sigstore/cosign/pkg/signature"
"github.com/sigstore/sigstore/pkg/signature"
)

func GetSigner(ctx context.Context, sk bool, keyRef string) (signature.Signer, error) {
if sk {
//nolint: staticcheck
pivKey, err := pivkey.GetKeyWithSlot("signature")
//nolint: staticcheck
if err != nil {
return nil, err
}
return pivKey.SignerVerifier()
}
// A key reference was provided.
// First try to load it as a regular PEM encoded private key.
signer, err := signature.LoadSignerFromPEMFile(keyRef, crypto.SHA256, nil)
if err != nil {
var innerError error
signer, innerError = csignature.SignerVerifierFromKeyRef(ctx, keyRef, nil)
if innerError != nil {
// Only print this message if both attempts failed.
// As there is a natual fallthrough here, always
// logging the first error could be noisy.
fmt.Printf("failed to load key as PEM encoded: %s, trying other methods: ", err)
return nil, innerError
}

}
return signer, nil
}

func GetVerifier(ctx context.Context, keyRef string) (signature.Verifier, error) {
verifier, err := signature.LoadVerifierFromPEMFile(keyRef, crypto.SHA256)

return verifier, err
}

// The DSSE Pre-Authentication Encoding
// https://github.com/secure-systems-lab/dsse/blob/master/protocol.md#signature-definition
func PAE(challenge, nonce string) []byte {
return []byte(fmt.Sprintf("key-kop-v1 %d %s %d %s",
len(challenge), challenge,
len(nonce), nonce))
}
11 changes: 5 additions & 6 deletions cmd/tuf/app/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ package app

import (
"context"
"encoding/json"
"flag"
"fmt"
"io"
Expand Down Expand Up @@ -80,11 +79,11 @@ func Init() *ffcli.Command {
Name: "init",
ShortUsage: "tuf init initializes a new TUF repository",
ShortHelp: "tuf init initializes a new TUF repository",
LongHelp: `tuf init initializes a new TUF repository to the
specified repository directory. It will create unpopulated directories
LongHelp: `tuf init initializes a new TUF repository to the
specified repository directory. It will create unpopulated directories
keys/, staged/, and staged/targets under the repository with threshold 3
and a 4 month expiration.

EXAMPLES
# initialize repository at ceremony/YYYY-MM-DD
tuf init -repository ceremony/YYYY-MM-DD`,
Expand Down Expand Up @@ -137,7 +136,7 @@ func Init() *ffcli.Command {
// Revoked keys will be automatically calculated given the previous root and the signers in directory.
// Signature placeholders for each key will be added to the root.json and targets.json file.
func InitCmd(ctx context.Context, directory, previous string,
threshold int, targetsConfig map[string]json.RawMessage,
threshold int, targetsConfig *prepo.TargetMetaConfig,
targetsDir string,
snapshotRef string, timestampRef string,
deprecatedKeyFormat bool) error {
Expand Down Expand Up @@ -204,7 +203,7 @@ func InitCmd(ctx context.Context, directory, previous string,
// Add targets (copy them into the repository and add them to the targets.json)
// Add the new targets in the config.
expectedTargets := make(map[string]bool)
for tt, custom := range targetsConfig {
for tt, custom := range targetsConfig.Add {
from, err := os.Open(filepath.Join(targetsDir, tt))
if err != nil {
return err
Expand Down
Loading