Skip to content

Commit

Permalink
Improve logic for signing MUD files and verifying signatures
Browse files Browse the repository at this point in the history
  • Loading branch information
hslatman committed Jul 29, 2021
1 parent aedfcf8 commit ef7ce66
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 209 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ The access control policies described in a MUD File allow network controllers to
## Things that can be done

* Fix (most) TODOs ... :-)
* Improve README.md
* Add 'Use' to commands
* Add tests
* Replace calls to fmt with proper logging / output
* Allow the tool to be chained (i.e. use STDIN/STDOUT, pipes, etc.)
* A command for generating MUD files (from pcap or some different way)
Expand Down
8 changes: 0 additions & 8 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,6 @@ func initConfig() {
// }
}

func fileExists(file string) bool {
_, err := os.Stat(file)
if err != nil {
return !os.IsNotExist(err)
}
return true
}

func dirExists(dir string) bool {
s, err := os.Stat(dir)
if err != nil {
Expand Down
208 changes: 54 additions & 154 deletions cmd/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,11 @@ limitations under the License.
package cmd

import (
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"io/ioutil"
"log"
"math/big"
"net/url"
"os"
fp "path/filepath"
"strings"
"time"
Expand All @@ -37,12 +31,11 @@ import (
"github.com/openconfig/ygot/ygot"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"go.step.sm/crypto/keyutil"
"go.step.sm/crypto/pemutil"
)

var baseURLFlag string
var ignoreExistingSignatureFlag bool
var normalizeFlag bool

// signCmd represents the sign command
var signCmd = &cobra.Command{
Expand All @@ -59,39 +52,42 @@ var signCmd = &cobra.Command{
if err != nil {
return errors.Wrap(err, "could not get contents")
}
if normalizeFlag {
json, err := internal.JSON(mudfile)
if err != nil {
return errors.Wrap(err, "normalizing MUD file failed")
}
data = []byte(json)
}

// TODO: provide option to first emit standard MUD JSON and use the JSON
// representation of that as the material to be signed?
existingMudUrl, err := internal.MUDURL(mudfile)
if err != nil {
return errors.Wrap(err, "retrieving MUD URL from MUD failed")
}

// TODO: look into this logic: if a signature path is know, the signature may already
// exist or not yet. If it does already exist, we may want to re-sign the MUD file.
// The location of the new signature can be assumed to be the same in the end.
// if !ignoreExistingSignatureFlag && mudHasSignature(mudfile) {
// return fmt.Errorf("this MUD already has a signature available at: %s", *mudfile.Mud.MudSignature)
// }
// existingMudSignatureUrl, err := internal.MUDSignatureURL(mudfile)
// if err != nil {
// return errors.Wrap(err, "retrieving MUD signature URL from MUD failed")
// }

existingMudUrl, err := internal.MUDURL(mudfile)
fmt.Println("existing mud url: ", existingMudUrl)
if err != nil {
return errors.Wrap(err, "retrieving MUD URL from MUD failed")
}

existingMudSignatureUrl, err := internal.MUDSignatureURL(mudfile)
fmt.Println("existing mud signature: ", existingMudSignatureUrl)
mudFilename, err := internal.MUDFilename(filepath)
if err != nil {
return errors.Wrap(err, "retrieving MUD signature URL from MUD failed")
return errors.Wrap(err, "retrieving MUD filename failed")
}

signatureFilename, err := internal.SignatureFilename(filepath)
fmt.Println("signature path: ", signatureFilename)
if err != nil {
return errors.Wrap(err, "retrieving signature path from MUD failed")
}

newMudURL := existingMudUrl
fmt.Println("new MUD url: ", newMudURL)
newSignatureURL := internal.NewMUDSignatureURL(existingMudUrl, signatureFilename)
fmt.Println("new signature url: ", newSignatureURL)

if baseURLFlag != "" {
newMudURL, err = rewriteBase(newMudURL, baseURLFlag)
Expand All @@ -104,26 +100,6 @@ var signCmd = &cobra.Command{
}
}

fmt.Println("new MUD url: ", newMudURL)
fmt.Println("new signature url: ", newSignatureURL)

//var signatureURL *url.URL
// if baseURLFlag != "" {
// baseURL, err := url.Parse(baseURLFlag)
// if err != nil {
// return errors.Wrap(err, "failed parsing base URL")
// }
// signatureURL = baseURL
// signatureURL.Path = signaturePath // TODO: support path with multiple segments
// } else {
// signatureURL = mudURL
// signatureURL.Path = signaturePath // TODO: support path with multiple segments
// }

// TODO: update Mudfile with location for signature? Needs to be clear that it has indeed be changed.
// TODO: if signing a local file, provide argument for the full path or directory for the signature file, so
// that the right value can be added to the MUD file before signing.

copy, err := ygot.DeepCopy(mudfile)
if err != nil {
return errors.Wrap(err, "creating deep copy of MUD YANG representation failed")
Expand All @@ -134,7 +110,7 @@ var signCmd = &cobra.Command{
return errors.New("the output MUD YANG is not a *mudyang.Mudfile")
}

// TODO: change other properties?
// TODO: change more properties?
mudURLString := newMudURL.String()
copyMUDFile.Mud.MudUrl = &mudURLString
signatureURLString := newSignatureURL.String()
Expand All @@ -146,144 +122,69 @@ var signCmd = &cobra.Command{
}

// TODO: can the diff be printed nicely (easily)? It seems to be some text values ...
log.Println("diff: ", diff)
//log.Println("diff: ", diff)

differencesFound := len(diff.GetDelete())+len(diff.GetUpdate()) > 0
if differencesFound {
// TODO: in case we're using the copy, we probably also need to update the updated_at
mudHadDifferences := len(diff.GetDelete())+len(diff.GetUpdate()) > 0 || normalizeFlag
if mudHadDifferences {
now := time.Now().Format("2006-01-02T15:04:05Z07:00")
copyMUDFile.Mud.LastUpdate = &now
json, err := internal.JSON(copyMUDFile)
if err != nil {
return errors.Wrap(err, "getting JSON representation of MUD file failed")
}
data = []byte(json)
}

fmt.Println(data)

// TODO: allow to provide a cert and key (and/or fallback to some stored in .mud dir?)
var cert *x509.Certificate
var key crypto.PrivateKey
keyFile := fp.Join(mudRootDir, "key.pem")
certFile := fp.Join(mudRootDir, "cert.pem")
if !fileExists(keyFile) {
certBytes, keyBytes := generateKey() // TODO: this logic should probably go somewhere different; a key + CSR should be created and sent to CA for a signed cert.
cert, err = x509.ParseCertificate(certBytes)
if err != nil {
return errors.Wrap(err, "parsing certificate failed")
}
_, err = pemutil.Serialize(cert, pemutil.ToFile(certFile, 0600))
if err != nil {
return errors.Wrapf(err, "serializing certificate to %s failed", certFile)
}
key, err = x509.ParsePKCS8PrivateKey(keyBytes)
if err != nil {
return errors.Wrap(err, "parsing private key failed")
}
_, err = pemutil.Serialize(key, pemutil.ToFile(keyFile, 0600), pemutil.WithPassword([]byte("1234"))) // TODO: provide password or prompt for it
if err != nil {
return errors.Wrapf(err, "serializing private key to %s failed", keyFile)
}
} else {
cert, err = pemutil.ReadCertificate(certFile)
if err != nil {
return errors.Wrapf(err, "reading certificate from %s failed", certFile)
}
k, err := pemutil.Read(keyFile, pemutil.WithPassword([]byte("1234")))
if err != nil {
return errors.Wrapf(err, "reading private key from %s failed", keyFile)
}
key = k
keyFilepath := fp.Join(mudRootDir, "key.pem")
certFilepath := fp.Join(mudRootDir, "cert.pem")
cert, signer, err := internal.LoadOrCreateKey(keyFilepath, certFilepath)
if err != nil {
return errors.Wrap(err, "loading/creating private key failed")
}

fmt.Println(cert)
fmt.Println(key)
fmt.Println(fmt.Sprintf("%T", key))

// TODO: add intermediates? Or how to integrate with a (private, trusted) CA? RFC says that they MUST be added.
certs := []*x509.Certificate{cert}

// TODO: allow signing with some other signer, too?
signer, ok := key.(crypto.Signer)
if !ok {
return errors.New("key is not a signer")
}

// TODO: add signed timestamp?
signature, err := cms.SignDetached(data, certs, signer)
if err != nil {
return errors.Wrap(err, "signing data failed")
}

// TODO: write to different location, based on signaturepath and close to the MUD file
// TODO: also provide an option to encode it to PEM instead?
newSignaturePath := fp.Join(mudRootDir, "signature.der")
err = ioutil.WriteFile(newSignaturePath, signature, 0644)
// TODO: if MUD file is local file (not URL), the put it next to the MUD file?
outputDir := fp.Join(mudRootDir, "files")
if !dirExists(outputDir) {
os.MkdirAll(outputDir, 0700)
}

// TODO: also provide an option to encode it to PEM instead? Have only seend DER examples, though.
newSignatureFilepath := fp.Join(outputDir, signatureFilename)
err = ioutil.WriteFile(newSignatureFilepath, signature, 0644)
if err != nil {
return errors.Wrap(err, "writing DER signature failed")
}

// TODO: if differences were found, we also should store the new
// MUD file, so that it can be uploaded later.

fmt.Println(signature)
log.Printf("MUD signature successfully written to %s\n", newSignatureFilepath)

// TODO: print output on how/where to store the file + signature?
if mudHadDifferences {
newMUDFilepath := fp.Join(outputDir, mudFilename)
err = ioutil.WriteFile(newMUDFilepath, data, 0644)
if err != nil {
return errors.Wrap(err, "writing DER signature failed")
}
log.Printf("Updated MUD file written to %s\n", newMUDFilepath)
}

log.Println("MUD file signed successfully")
return nil
},
}

func publicKey(priv interface{}) interface{} {
switch k := priv.(type) {
case *rsa.PrivateKey:
return &k.PublicKey
case *ecdsa.PrivateKey:
return &k.PublicKey
default:
return nil
}
}

func generateKey() (certBytes, keyBytes []byte) {
priv, err := keyutil.GenerateKey("EC", "P-256", 0)
//priv, err := keyutil.GenerateKey("RSA", "", 2048)
if err != nil {
log.Fatal(err)
}

template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Organization: []string{"mudsign example organization"},
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour * 24 * 180),
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, // TODO: fix usage? Would actually be better if this was more like a separate command, I guess
ExtKeyUsage: []x509.ExtKeyUsage{},
IsCA: true,
BasicConstraintsValid: true,
}

derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv)
if err != nil {
log.Fatalf("failed to create certificate: %s", err)
}

keyBytes, err = x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
log.Fatalf("failed to marshal private key: %s", err)
}

return derBytes, keyBytes
}

func mudHasSignature(mud *mudyang.Mudfile) bool {
return mud.Mud.MudSignature != nil
}
// func mudHasSignature(mud *mudyang.Mudfile) bool {
// return mud.Mud.MudSignature != nil
// }

func rewriteBase(u *url.URL, baseURLString string) (*url.URL, error) {
// TODO: check this works as expected
// TODO: check/test this works as expected in all/most cases
base, err := url.Parse(baseURLString)
if err != nil {
return nil, err
Expand All @@ -306,9 +207,8 @@ func rewriteBase(u *url.URL, baseURLString string) (*url.URL, error) {
func init() {
rootCmd.AddCommand(signCmd)

// TODO: provide a flag that uses a base URL for the MUD URL and signature to exist in the file?

signCmd.PersistentFlags().StringVarP(&baseURLFlag, "base-url", "u", "", "Base URL to use for MUD URL and signature location")
signCmd.PersistentFlags().StringVarP(&signatureFlag, "signature", "s", "", "Location of signature file to set")
signCmd.PersistentFlags().BoolVar(&ignoreExistingSignatureFlag, "ignore-existing-signature", false, "Ignore")
signCmd.PersistentFlags().BoolVar(&ignoreExistingSignatureFlag, "ignore-existing-signature", false, "Ignore case in which MUD already has a signature")
signCmd.PersistentFlags().BoolVarP(&normalizeFlag, "normalize", "n", false, "Normalize the MUD JSON according to default mud.yang.go order")
}
Loading

0 comments on commit ef7ce66

Please sign in to comment.