sign.go 5.14 KB
Newer Older
1
package cmd
2
3
4
5
6
7
8
9
10
11

import (
	"crypto/ecdsa"
	"crypto/rand"
	"crypto/sha256"
	"crypto/x509"
	"encoding/asn1"
	"encoding/pem"
	"io/ioutil"
	"math/big"
12
	"os"
13
	"path/filepath"
14
15
	"regexp"
	"strconv"
16
	"strings"
17
	"time"
18

19
	"github.com/go-errors/errors"
20
21
	"github.com/privacybydesign/irmago"
	"github.com/privacybydesign/irmago/internal/fs"
22
	"github.com/spf13/cobra"
23
24
)

25
26
// signCmd represents the sign command
var signCmd = &cobra.Command{
27
	Use:   "sign [privatekey] [path]",
28
	Short: "Sign a scheme directory",
29
30
31
32
	Long: `Sign a scheme manager directory, using the specified ECDSA key. Both arguments are optional; "sk.pem" and the working directory are the defaults. Outputs an index file, signature over the index file, and the public key in the specified directory.

Careful: this command could fail and invalidate or destroy your scheme manager directory! Use this only if you can restore it from git or backups.`,
	Args: cobra.MaximumNArgs(2),
33
34
35
36
37
38
39
40
41
42
43
44
45
46
	RunE: func(cmd *cobra.Command, args []string) error {
		// Validate arguments
		var err error
		var sk, confpath string
		switch len(args) {
		case 0:
			sk = "sk.pem"
			confpath, err = os.Getwd()
		case 1:
			sk = args[0]
			confpath, err = os.Getwd()
		case 2:
			sk = args[0]
			confpath, err = filepath.Abs(args[1])
Sietse Ringers's avatar
Sietse Ringers committed
47
48
49
		}
		if err != nil {
			return errors.WrapPrefix(err, "Invalid path", 0)
50
51
52
53
54
55
56
57
58
59
60
		}

		privatekey, err := readPrivateKey(sk)
		if err != nil {
			return errors.WrapPrefix(err, "Failed to read private key:", 0)
		}

		if err = fs.AssertPathExists(confpath); err != nil {
			return err
		}

61
62
63
64
65
		skipverification, err := cmd.Flags().GetBool("noverification")
		if err != nil {
			return err
		}
		if err := signManager(privatekey, confpath, skipverification); err != nil {
66
67
68
			die("Failed to sign scheme", err)
		}
		return nil
69
70
71
72
	},
}

func init() {
73
	schemeCmd.AddCommand(signCmd)
74
75

	signCmd.Flags().BoolP("noverification", "n", false, "Skip verification of the scheme after signing it")
76
77
}

78
func signManager(privatekey *ecdsa.PrivateKey, confpath string, skipverification bool) error {
79
80
	// Write timestamp
	bts := []byte(strconv.FormatInt(time.Now().Unix(), 10) + "\n")
81
	if err := ioutil.WriteFile(filepath.Join(confpath, "timestamp"), bts, 0644); err != nil {
82
		return errors.WrapPrefix(err, "Failed to write timestamp", 0)
83
84
	}

85
86
	// Traverse dir and add file hashes to index
	var index irma.SchemeManagerIndex = make(map[string]irma.ConfigurationFileHash)
87
	err := filepath.Walk(confpath, func(path string, info os.FileInfo, err error) error {
88
89
90
		return calculateFileHash(path, info, err, confpath, index)
	})
	if err != nil {
91
		return errors.WrapPrefix(err, "Failed to calculate file index:", 0)
92
93
	}

94
95
	// Write index
	bts = []byte(index.String())
96
	if err := ioutil.WriteFile(filepath.Join(confpath, "index"), bts, 0644); err != nil {
97
		return errors.WrapPrefix(err, "Failed to write index", 0)
98
99
100
101
102
103
	}

	// Create and write signature
	indexHash := sha256.Sum256(bts)
	r, s, err := ecdsa.Sign(rand.Reader, privatekey, indexHash[:])
	if err != nil {
104
		return errors.WrapPrefix(err, "Failed to sign index:", 0)
105
106
107
	}
	sigbytes, err := asn1.Marshal([]*big.Int{r, s})
	if err != nil {
108
		return errors.WrapPrefix(err, "Failed to serialize signature:", 0)
109
	}
110
	if err = ioutil.WriteFile(filepath.Join(confpath, "index.sig"), sigbytes, 0644); err != nil {
111
		return errors.WrapPrefix(err, "Failed to write index.sig", 0)
112
113
114
115
116
	}

	// Write public key
	bts, err = x509.MarshalPKIXPublicKey(&privatekey.PublicKey)
	if err != nil {
117
		return errors.WrapPrefix(err, "Failed to serialize public key", 0)
118
119
	}
	pemEncodedPub := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: bts})
120
	if err := ioutil.WriteFile(filepath.Join(confpath, "pk.pem"), pemEncodedPub, 0644); err != nil {
121
122
123
124
125
126
127
128
129
130
131
132
		return errors.WrapPrefix(err, "Failed to write public key", 0)
	}

	if skipverification {
		return nil
	}

	// Verify that our folder is a valid scheme
	if err := RunVerify(confpath, false); err != nil {
		die("Scheme was signed but verification failed", err)
	}
	return nil
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
}

func readPrivateKey(path string) (*ecdsa.PrivateKey, error) {
	bts, err := ioutil.ReadFile(path)
	if err != nil {
		return nil, err
	}
	block, _ := pem.Decode(bts)
	return x509.ParseECPrivateKey(block.Bytes)
}

func calculateFileHash(path string, info os.FileInfo, err error, confpath string, index irma.SchemeManagerIndex) error {
	if err != nil {
		return err
	}
148
	// Skip stuff we don't want
149
150
	if info.IsDir() || // Can only sign files
		strings.HasSuffix(path, "index") || // Skip the index file itself
151
152
		strings.Contains(filepath.ToSlash(path), "/.git/") || // No need to traverse .git dirs, can take quite long
		strings.Contains(filepath.ToSlash(path), "/PrivateKeys/") { // Don't sign private keys
153
154
155
156
157
158
159
		return nil
	}
	// Skip everything except the stuff we do want
	if !strings.HasSuffix(path, ".xml") &&
		!strings.HasSuffix(path, ".png") &&
		!regexp.MustCompile("kss-\\d+\\.pem$").Match([]byte(filepath.Base(path))) &&
		filepath.Base(path) != "timestamp" {
160
161
162
163
164
165
166
167
168
169
170
171
172
173
		return nil
	}

	bts, err := ioutil.ReadFile(path)
	if err != nil {
		return err
	}
	relativePath, err := filepath.Rel(confpath, path)
	if err != nil {
		return err
	}
	relativePath = filepath.Join(filepath.Base(confpath), relativePath)

	hash := sha256.Sum256(bts)
174
	index[filepath.ToSlash(relativePath)] = hash[:]
175
176
	return nil
}