Skip to content

Commit ea0ce60

Browse files
committed
add keys generate command to generate the encryption keys required for secure boot
1 parent f014be1 commit ea0ce60

File tree

6 files changed

+627
-0
lines changed

6 files changed

+627
-0
lines changed

cli/cli.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"github.com/arduino/arduino-cli/cli/feedback"
3535
"github.com/arduino/arduino-cli/cli/generatedocs"
3636
"github.com/arduino/arduino-cli/cli/globals"
37+
"github.com/arduino/arduino-cli/cli/keys"
3738
"github.com/arduino/arduino-cli/cli/lib"
3839
"github.com/arduino/arduino-cli/cli/monitor"
3940
"github.com/arduino/arduino-cli/cli/outdated"
@@ -93,6 +94,7 @@ func createCliCommandTree(cmd *cobra.Command) {
9394
cmd.AddCommand(core.NewCommand())
9495
cmd.AddCommand(daemon.NewCommand())
9596
cmd.AddCommand(generatedocs.NewCommand())
97+
cmd.AddCommand(keys.NewCommand())
9698
cmd.AddCommand(lib.NewCommand())
9799
cmd.AddCommand(monitor.NewCommand())
98100
cmd.AddCommand(outdated.NewCommand())

cli/keys/generate.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 2020 ARDUINO SA (http://www.arduino.cc/)
4+
//
5+
// This software is released under the GNU General Public License version 3,
6+
// which covers the main part of arduino-cli.
7+
// The terms of this license can be found at:
8+
// https://www.gnu.org/licenses/gpl-3.0.en.html
9+
//
10+
// You can be released from the requirements of the above licenses by purchasing
11+
// a commercial license. Buying such a license is mandatory if you want to
12+
// modify or otherwise use the software for commercial activities involving the
13+
// Arduino software without disclosing the source code of your own applications.
14+
// To purchase a commercial license, send an email to [email protected].
15+
16+
package keys
17+
18+
import (
19+
"context"
20+
"os"
21+
22+
"github.com/arduino/arduino-cli/cli/errorcodes"
23+
"github.com/arduino/arduino-cli/cli/feedback"
24+
"github.com/arduino/arduino-cli/commands/keys"
25+
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
26+
"github.com/sirupsen/logrus"
27+
"github.com/spf13/cobra"
28+
)
29+
30+
var (
31+
keyName []string // Name of the custom keys to generate. Can be used multiple times for multiple security keys.
32+
algorithmType string // Algorithm type to use
33+
keysKeychain string // Path of the dir where to save the custom keys
34+
)
35+
36+
func initGenerateCommand() *cobra.Command {
37+
generateCommand := &cobra.Command{
38+
Use: "generate",
39+
Short: tr("Generate the security keys."),
40+
Long: tr("Generate the security keys required for secure boot"),
41+
Example: "" +
42+
" " + os.Args[0] + " keys generate -t ecdsa-p256 --key-name ecdsa-p256-signing-key.pem --key-name ecdsa-p256-encrypt-key.pem --keys-keychain /home/user/Arduino/MyKeys\n" +
43+
" " + os.Args[0] + " keys generate --key-name ecdsa-p256-signing-key.pem",
44+
Args: cobra.NoArgs,
45+
Run: runGenerateCommand,
46+
}
47+
48+
generateCommand.Flags().StringVarP(&algorithmType, "type", "t", "ecdsa-p256", tr("Algorithm type to use"))
49+
generateCommand.Flags().StringArrayVar(&keyName, "key-name", []string{}, tr("Name of the custom key to generate. Can be used multiple times for multiple security keys."))
50+
generateCommand.MarkFlagRequired("key-name")
51+
generateCommand.RegisterFlagCompletionFunc("key-name", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
52+
defaultKeyNames := []string{"ecdsa-p256-signing-key.pem", "ecdsa-p256-encrypt-key.pem"}
53+
return defaultKeyNames, cobra.ShellCompDirectiveDefault
54+
})
55+
generateCommand.Flags().StringVar(&keysKeychain, "keys-keychain", "", tr("The path of the dir where to save the custom keys"))
56+
57+
return generateCommand
58+
}
59+
60+
func runGenerateCommand(command *cobra.Command, args []string) {
61+
62+
logrus.Info("Executing `arduino-cli keys generate`")
63+
64+
_, err := keys.Generate(context.Background(), &rpc.KeysGenerateRequest{
65+
AlgorithmType: algorithmType,
66+
KeyName: keyName,
67+
KeysKeychain: keysKeychain,
68+
})
69+
if err != nil {
70+
feedback.Errorf(tr("Error during Generate: %v"), err)
71+
os.Exit(errorcodes.ErrGeneric)
72+
}
73+
74+
feedback.Print(tr("Keys created in: %s", keysKeychain))
75+
}

cli/keys/keys.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 2020 ARDUINO SA (http://www.arduino.cc/)
4+
//
5+
// This software is released under the GNU General Public License version 3,
6+
// which covers the main part of arduino-cli.
7+
// The terms of this license can be found at:
8+
// https://www.gnu.org/licenses/gpl-3.0.en.html
9+
//
10+
// You can be released from the requirements of the above licenses by purchasing
11+
// a commercial license. Buying such a license is mandatory if you want to
12+
// modify or otherwise use the software for commercial activities involving the
13+
// Arduino software without disclosing the source code of your own applications.
14+
// To purchase a commercial license, send an email to [email protected].
15+
16+
package keys
17+
18+
import (
19+
"os"
20+
21+
"github.com/arduino/arduino-cli/i18n"
22+
"github.com/spf13/cobra"
23+
)
24+
25+
var tr = i18n.Tr
26+
27+
// NewCommand created a new `keys` command
28+
func NewCommand() *cobra.Command {
29+
keysCommand := &cobra.Command{
30+
Use: "keys",
31+
Short: tr("Arduino keys commands."),
32+
Long: tr("Arduino keys commands. Useful to operate on security keys"),
33+
Example: " " + os.Args[0] + " keys generate --key-name ecdsa-p256-signing-key.pem --keys-keychain /home/user/Arduino/MyKeys",
34+
}
35+
36+
keysCommand.AddCommand(initGenerateCommand())
37+
38+
return keysCommand
39+
}

commands/keys/generate.go

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 2020 ARDUINO SA (http://www.arduino.cc/)
4+
//
5+
// This software is released under the GNU General Public License version 3,
6+
// which covers the main part of arduino-cli.
7+
// The terms of this license can be found at:
8+
// https://www.gnu.org/licenses/gpl-3.0.en.html
9+
//
10+
// You can be released from the requirements of the above licenses by purchasing
11+
// a commercial license. Buying such a license is mandatory if you want to
12+
// modify or otherwise use the software for commercial activities involving the
13+
// Arduino software without disclosing the source code of your own applications.
14+
// To purchase a commercial license, send an email to [email protected].
15+
16+
package keys
17+
18+
import (
19+
"bytes"
20+
"context"
21+
"crypto/ecdsa"
22+
"crypto/elliptic"
23+
"crypto/rand"
24+
"crypto/x509"
25+
"encoding/pem"
26+
"errors"
27+
"fmt"
28+
"os"
29+
"strings"
30+
31+
"github.com/arduino/arduino-cli/arduino"
32+
"github.com/arduino/arduino-cli/i18n"
33+
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
34+
"github.com/arduino/go-paths-helper"
35+
)
36+
37+
var tr = i18n.Tr
38+
39+
// Generate creates a new set of security keys via gRPC
40+
func Generate(ctx context.Context, req *rpc.KeysGenerateRequest) (*rpc.KeysGenerateResponse, error) {
41+
// check if the keychain is passed as argument
42+
keysKeychainDir := paths.New(req.GetKeysKeychain())
43+
if keysKeychainDir == nil {
44+
keysKeychainDir = paths.New(".")
45+
keysKeychainDir, _ = keysKeychainDir.Abs()
46+
}
47+
pathExists, err := keysKeychainDir.ExistCheck()
48+
if !pathExists {
49+
if err = keysKeychainDir.MkdirAll(); err != nil {
50+
return nil, &arduino.PermissionDeniedError{Message: tr("Cannot create directory"), Cause: err}
51+
}
52+
}
53+
if err != nil {
54+
return nil, &arduino.PermissionDeniedError{Message: tr("Cannot verify if the directory %s exists", keysKeychainDir), Cause: err}
55+
}
56+
57+
// check the number of keynames passed
58+
if len(req.GetKeyName()) == 0 {
59+
return nil, &arduino.InvalidArgumentError{Message: tr("Wrong number of key names specified, please use at least one")}
60+
}
61+
62+
// check if the algorithm has been specified, set the default if not
63+
algorithmType := req.GetAlgorithmType()
64+
if algorithmType == "" {
65+
algorithmType = "ecdsa-p256"
66+
}
67+
68+
// do the actual key generation
69+
for _, key := range req.GetKeyName() {
70+
// build the path where to save the security key
71+
privKeyPath := keysKeychainDir.Join("priv_" + key)
72+
pubKeyPath := keysKeychainDir.Join("pub_" + key)
73+
err = doKeyGen(privKeyPath, pubKeyPath, algorithmType)
74+
if err != nil {
75+
return nil, &arduino.FileCreationFailedError{Message: tr("Cannot create file"), Cause: err}
76+
}
77+
}
78+
return &rpc.KeysGenerateResponse{KeysKeychain: keysKeychainDir.String()}, nil
79+
}
80+
81+
// adapted from https://git.furworks.de/Zephyr/mcuboot/src/commit/3869e760901a27adff47ccaea803a42f1b0169c0/imgtool/imgtool.go#L69
82+
// doKeyGen will take the paths of the public and private keys to write and will generate keys according to keyType
83+
func doKeyGen(privKeyPath, pubKeyPath *paths.Path, keyType string) (err error) {
84+
var priv509, pubAsn1 []byte
85+
var privPemType, pubPemType string
86+
switch keyType {
87+
case "ecdsa-p256":
88+
priv509, pubAsn1, err = genEcdsaP256()
89+
privPemType = "PRIVATE KEY"
90+
pubPemType = "PUBLIC KEY"
91+
// support for multiple algorithms can be added there
92+
default:
93+
err = errors.New(tr("Unsupported algorithm: %s", keyType))
94+
}
95+
96+
if err != nil {
97+
return err
98+
}
99+
keysKeychainDir := privKeyPath.Parent()
100+
101+
// create the private key file
102+
if privKeyPath.Exist() {
103+
return errors.New(tr("File already exists: %s", privKeyPath))
104+
}
105+
privKeyFile, err := privKeyPath.Create()
106+
if err != nil {
107+
return err
108+
}
109+
defer privKeyFile.Close()
110+
111+
// create the public key file
112+
if pubKeyPath.Exist() {
113+
return errors.New(tr("File already exists: %s", pubKeyPath))
114+
}
115+
pubKeyFile, err := pubKeyPath.Create()
116+
if err != nil {
117+
return err
118+
}
119+
defer pubKeyFile.Close()
120+
121+
// create the private header files
122+
privHeader := strings.TrimSuffix(privKeyPath.Base(), privKeyPath.Ext())
123+
privHeaderPath := keysKeychainDir.Join(privHeader + ".h")
124+
if privHeaderPath.Exist() {
125+
return errors.New(tr("File already exists: %s", privHeaderPath))
126+
}
127+
privHeaderFile, err := privHeaderPath.Create()
128+
if err != nil {
129+
return err
130+
}
131+
defer privHeaderFile.Close()
132+
133+
// create the public header files
134+
pubHeader := strings.TrimSuffix(pubKeyPath.Base(), pubKeyPath.Ext())
135+
pubHeaderPath := keysKeychainDir.Join(pubHeader + ".h")
136+
if pubHeaderPath.Exist() {
137+
return errors.New(tr("File already exists: %s", pubHeaderPath))
138+
}
139+
pubHeaderFile, err := pubHeaderPath.Create()
140+
if err != nil {
141+
return err
142+
}
143+
defer pubHeaderFile.Close()
144+
145+
privBlock := pem.Block{
146+
Type: privPemType,
147+
Bytes: priv509,
148+
}
149+
err = pem.Encode(privKeyFile, &privBlock)
150+
if err != nil {
151+
return err
152+
}
153+
err = genCFile(privHeaderFile, priv509, "priv")
154+
if err != nil {
155+
return err
156+
}
157+
158+
pubBlock := pem.Block{
159+
Type: pubPemType,
160+
Bytes: pubAsn1,
161+
}
162+
err = pem.Encode(pubKeyFile, &pubBlock)
163+
if err != nil {
164+
return err
165+
}
166+
err = genCFile(pubHeaderFile, pubAsn1, "pub")
167+
if err != nil {
168+
return err
169+
}
170+
return nil
171+
}
172+
173+
// genEcdsaP256 will generate private and public ecdsap256 keypair and return them
174+
// it will encode the private PKCS #8, ASN.1 DER form, and for the public will use the PKIX, ASN.1 DER form
175+
// adapted from https://git.furworks.de/Zephyr/mcuboot/src/commit/3869e760901a27adff47ccaea803a42f1b0169c0/imgtool/imgtool.go#L165
176+
func genEcdsaP256() ([]byte, []byte, error) {
177+
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
178+
if err != nil {
179+
return nil, nil, err
180+
}
181+
182+
keyPriv, err := x509.MarshalPKCS8PrivateKey(priv)
183+
if err != nil {
184+
return nil, nil, err
185+
}
186+
187+
pub := priv.Public()
188+
keyPub, err := x509.MarshalPKIXPublicKey(pub)
189+
if err != nil {
190+
return nil, nil, err
191+
}
192+
193+
return keyPriv, keyPub, nil
194+
}
195+
196+
// genCFile will take a file as input, the byte slice that represents the key and the type of the key.
197+
// If will then write the file and report an error if one is found
198+
func genCFile(file *os.File, bytes []byte, t string) (err error) {
199+
fileContent := fmt.Sprintf(`/* Autogenerated, do not edit */
200+
const unsigned char rsa_%s_key[] = {
201+
%s
202+
};
203+
const unsigned int ec_%s_key_len = %d;
204+
`, t, formatCData(bytes), t, len(bytes))
205+
_, err = file.WriteString(fileContent)
206+
return err
207+
}
208+
209+
// formatCData will take the byte slice representing a key and format correctly as "C" data. It will return it as a string
210+
// taken and adapted from https://git.furworks.de/Zephyr/mcuboot/src/commit/3869e760901a27adff47ccaea803a42f1b0169c0/imgtool/imgtool.go#L313
211+
func formatCData(data []byte) string {
212+
buf := new(bytes.Buffer)
213+
indText := strings.Repeat("\t", 1)
214+
for i, b := range data {
215+
if i%8 == 0 {
216+
if i > 0 {
217+
fmt.Fprintf(buf, "\n%s", indText)
218+
}
219+
} else {
220+
fmt.Fprintf(buf, " ")
221+
}
222+
fmt.Fprintf(buf, "0x%02x,", b)
223+
}
224+
return buf.String()
225+
}

0 commit comments

Comments
 (0)