diff --git a/.gitignore b/.gitignore index bd53762..bc768b9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +external-signer-tpm # ---> Go # If you prefer the allow list template instead of the deny list, see community template: # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore diff --git a/README.md b/README.md index 80e7e04..cae1eed 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ # external-signer-tpm +> Reference: https://github.com/google/go-tpm/tree/main/examples/tpm-sign + diff --git a/common.go b/common.go new file mode 100644 index 0000000..6c949b4 --- /dev/null +++ b/common.go @@ -0,0 +1,7 @@ +package main + +const ( + srkAuthEnvVar = "TPM_SRK_AUTH" + usageAuthEnvVar = "TPM_USAGE_AUTH" + migrationAuthEnvVar = "TPM_MIGRATION_AUTH" +) diff --git a/generate.go b/generate.go new file mode 100644 index 0000000..70c2e16 --- /dev/null +++ b/generate.go @@ -0,0 +1,136 @@ +package main + +import ( + "crypto/sha1" + "crypto/x509" + "encoding/base64" + "encoding/json" + "fmt" + "os" + "strconv" + "strings" + + "github.com/google/go-tpm/tpm" + "github.com/urfave/cli/v2" +) + +func buildGenerateKeyCommand() *cli.Command { + return &cli.Command{ + Name: "generate_key", + Usage: "Generate key", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "parameter", + Usage: "The path to the TPM device to use", + DefaultText: "/dev/tpm0", + }, + &cli.StringFlag{ + Name: "keyblob", + Usage: "Output path of the generated keyblob", + DefaultText: "keyblob", + }, + &cli.StringFlag{ + Name: "public-key", + Usage: "Output path of the generated keyblob's public key", + DefaultText: "publickey", + }, + &cli.StringFlag{ + Name: "pcrs", + Usage: "A comma-separated list of PCR numbers against which the generated key will be bound. If blank, it will not be bound to any PCR values.", + }, + }, + Action: func(ctx *cli.Context) error { + tpmname := ctx.String("tpmname") + keyblob := ctx.String("keyblob") + publicKey := ctx.String("public-key") + pcrs := ctx.String("pcrs") + + generateAction(tpmname, keyblob, publicKey, pcrs) + return nil + }, + } +} + +func generateAction(tpmname, keyblobPath, pubKeyPath, pcrsStr string) { + + var pcrs []int + if pcrsStr != "" { + for _, pcr := range strings.Split(pcrsStr, ",") { + pcrNum, err := strconv.Atoi(pcr) + if err != nil { + fmt.Fprintf(os.Stderr, "Bad value in pcrs argument: %s\n", pcr) + return + } + pcrs = append(pcrs, pcrNum) + } + } + + rwc, err := tpm.OpenTPM(tpmname) + if err != nil { + fmt.Fprintf(os.Stderr, "Couldn't open the TPM file %s: %s\n", tpmname, err) + return + } + defer rwc.Close() + + // Compute the auth values as needed. + var srkAuth [20]byte + srkInput := os.Getenv(srkAuthEnvVar) + if srkInput != "" { + sa := sha1.Sum([]byte(srkInput)) + copy(srkAuth[:], sa[:]) + } + + var usageAuth [20]byte + usageInput := os.Getenv(usageAuthEnvVar) + if usageInput != "" { + ua := sha1.Sum([]byte(usageInput)) + copy(usageAuth[:], ua[:]) + } + + var migrationAuth [20]byte + migrationInput := os.Getenv(migrationAuthEnvVar) + if migrationInput != "" { + ma := sha1.Sum([]byte(migrationInput)) + copy(migrationAuth[:], ma[:]) + } + + keyblob, err := tpm.CreateWrapKey(rwc, srkAuth[:], usageAuth, migrationAuth, pcrs) + if err != nil { + fmt.Fprintf(os.Stderr, "Couldn't make a new signing key: %s\n", err) + return + } + fmt.Printf("Writing keyblob to %s\n", keyblobPath) + if err = os.WriteFile(keyblobPath, keyblob, 0644); err != nil { + fmt.Fprintf(os.Stderr, "Error writing keyblob file: %s\n", err) + return + } + + pubKey, err := tpm.UnmarshalRSAPublicKey(keyblob) + if err != nil { + fmt.Fprintf(os.Stderr, "Could not get public key: %s\n", err) + return + } + + pubKeyBytes, err := x509.MarshalPKIXPublicKey(pubKey) + if err != nil { + fmt.Fprintf(os.Stderr, "Could not marshal public key: %s\n", err) + return + } + fmt.Printf("Writing public key to %s\n", pubKeyPath) + if err = os.WriteFile(pubKeyPath, pubKeyBytes, 0644); err != nil { + fmt.Fprintf(os.Stderr, "Error writing public key file: %s\n", err) + return + } + + tpmKey := &TpmKey{ + Tpm: tpmname, + Keyblob: base64.StdEncoding.EncodeToString(keyblob), + } + + tpmKeyJson, err := json.Marshal(&tpmKey) + if err != nil { + fmt.Fprintf(os.Stderr, "Error marshal tmp key: %s\n", err) + return + } + println(base64.StdEncoding.EncodeToString(tpmKeyJson)) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7505541 --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module git.hatter.ink/external-signer-tpm + +go 1.24.1 + +require ( + github.com/google/go-tpm v0.9.5 + github.com/urfave/cli/v2 v2.27.6 +) + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + golang.org/x/sys v0.8.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..55b94ae --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU= +github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= +github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/main.go b/main.go new file mode 100644 index 0000000..a071de3 --- /dev/null +++ b/main.go @@ -0,0 +1,267 @@ +package main + +import ( + "crypto" + "crypto/sha1" + "crypto/x509" + "encoding/base64" + "encoding/json" + "fmt" + "os" + + "github.com/google/go-tpm/tpm" + "github.com/urfave/cli/v2" +) + +type TpmKey struct { + Tpm string `json:"tpm"` + Keyblob string `json:"keyblob"` +} + +type ErrorResponse struct { + Success bool `json:"success"` + Error string `json:"error"` +} + +type ExternalSpecResponse struct { + Success bool `json:"success"` + Agent string `json:"agent"` + Specification string `json:"specification"` + Commands []string `json:"commands"` +} + +type ExternalPublicKeyResponse struct { + Success bool `json:"success"` + PublicKeyBase64 string `json:"public_key_base64"` +} + +type ExternalSignatureResponse struct { + Success bool `json:"success"` + SignatureBase64 string `json:"signature_base64"` +} + +func printResponse(response any) error { + errorMessageJsonBytes, jsonErr := json.Marshal(response) + if jsonErr != nil { + return jsonErr + } else { + fmt.Println(string(errorMessageJsonBytes)) + return nil + } +} + +func printResponseSlient(response any) { + if err := printResponse(response); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "%v\n", err) + } +} + +func main() { + err := innerMain() + if err != nil { + errorMessage := &ErrorResponse{ + Success: false, + Error: fmt.Sprintf("%v", err), + } + if printResponse(errorMessage) != nil { + _, _ = fmt.Fprintf(os.Stderr, "%v\n", err) + } + os.Exit(1) + } +} + +func innerMain() error { + app := cli.App{ + Name: "external-signer-tpm", + Usage: "External signer TPM", + Commands: []*cli.Command{ + buildExternalSpecCommand(), + buildExternalPublicKeyCommand(), + buildExternalSignCommand(), + buildGenerateKeyCommand(), + }, + Action: func(ctx *cli.Context) error { + fmt.Println("External signer TPM, specification: https://openwebstandard.org/rfc1") + return nil + }, + } + + if err := app.Run(os.Args); err != nil { + return err + } + return nil +} + +func buildExternalSpecCommand() *cli.Command { + return &cli.Command{ + Name: "external_spec", + Usage: "External specification", + Flags: []cli.Flag{}, + Action: func(ctx *cli.Context) error { + externalSpecResponse := &ExternalSpecResponse{ + Success: true, + Agent: "external-signer-tpm/0.1.0", + Specification: "External/1.0.0-alpha", + Commands: []string{ + "external_public_key", + "external_sign", + }, + } + printResponseSlient(externalSpecResponse) + return nil + }, + } +} + +func buildExternalPublicKeyCommand() *cli.Command { + return &cli.Command{ + Name: "external_public_key", + Usage: "External public key", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "parameter", + Usage: "Parameter", + Required: true, + }, + }, + Action: func(ctx *cli.Context) error { + parameter := ctx.String("parameter") + + tpmKey, err := parseParameter(parameter) + if err != nil { + return fmt.Errorf("cannot parse parameter: %s", err) + } + + keyblob, err := base64.StdEncoding.DecodeString(tpmKey.Keyblob) + if err != nil { + return fmt.Errorf("cannot decode keyblob: %s", err) + } + pubKey, err := tpm.UnmarshalRSAPublicKey(keyblob) + if err != nil { + return fmt.Errorf("cannot get public key: %s", err) + } + + pubKeyBytes, err := x509.MarshalPKIXPublicKey(pubKey) + if err != nil { + return fmt.Errorf("cannot marshal public key: %s", err) + } + + externalPublicKeyResponse := &ExternalPublicKeyResponse{ + Success: true, + PublicKeyBase64: base64.StdEncoding.EncodeToString(pubKeyBytes), + } + + printResponseSlient(externalPublicKeyResponse) + return nil + }, + } +} + +func buildExternalSignCommand() *cli.Command { + return &cli.Command{ + Name: "external_sign", + Usage: "External sign", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "parameter", + Usage: "Parameter", + Required: true, + }, + &cli.StringFlag{ + Name: "alg", + Usage: "Algorithm", + Required: true, + }, + &cli.StringFlag{ + Name: "message-base64", + Usage: "Message base64 encoded", + Required: true, + }, + }, + Action: func(ctx *cli.Context) error { + parameter := ctx.String("parameter") + alg := ctx.String("alg") + messageBase64 := ctx.String("message-base64") + message, err := base64.StdEncoding.DecodeString(messageBase64) + if err != nil { + return fmt.Errorf("unmarshal --message-base64 failed: %v", err) + } + + println("ALG: ", alg) + + tpmKey, err := parseParameter(parameter) + if err != nil { + return fmt.Errorf("cannot parse parameter: %s", err) + } + + rwc, err := tpm.OpenTPM(tpmKey.Tpm) + if err != nil { + return fmt.Errorf("cannot open TPM %s, error: %s", tpmKey.Tpm, err) + } + defer rwc.Close() + + // Compute the auth values as needed. + var srkAuth [20]byte + srkInput := os.Getenv(srkAuthEnvVar) + if srkInput != "" { + sa := sha1.Sum([]byte(srkInput)) + copy(srkAuth[:], sa[:]) + } + + var usageAuth [20]byte + usageInput := os.Getenv(usageAuthEnvVar) + if usageInput != "" { + ua := sha1.Sum([]byte(usageInput)) + copy(usageAuth[:], ua[:]) + } + + var migrationAuth [20]byte + migrationInput := os.Getenv(migrationAuthEnvVar) + if migrationInput != "" { + ma := sha1.Sum([]byte(migrationInput)) + copy(migrationAuth[:], ma[:]) + } + + keyblob, err := base64.StdEncoding.DecodeString(tpmKey.Keyblob) + if err != nil { + return fmt.Errorf("cannot decode keyblob: %s", err) + } + keyHandle, err := tpm.LoadKey2(rwc, keyblob, srkAuth[:]) + if err != nil { + return fmt.Errorf("cannot load keyblob: %s", err) + } + defer tpm.CloseKey(rwc, keyHandle) + + hash := crypto.SHA256.New() + if _, err = hash.Write(message); err != nil { + return fmt.Errorf("error building hash of data: %s", err) + } + hashed := hash.Sum(nil) + signature, err := tpm.Sign(rwc, usageAuth[:], keyHandle, crypto.SHA256, hashed[:]) + if err != nil { + return fmt.Errorf("cannot perform sign operation: %s", err) + } + + externalSignatureResponse := ExternalSignatureResponse{ + Success: true, + SignatureBase64: base64.StdEncoding.EncodeToString(signature), + } + + printResponseSlient(externalSignatureResponse) + return nil + }, + } +} + +func parseParameter(parameter string) (*TpmKey, error) { + parameterBytes, err := base64.StdEncoding.DecodeString(parameter) + if err != nil { + return nil, fmt.Errorf("parse --parameter base64 failed: %v", err) + } + var tpmKey TpmKey + err = json.Unmarshal(parameterBytes, &tpmKey) + if err != nil { + return nil, fmt.Errorf("parse --parameter base64 failed: %v", err) + } + return &tpmKey, nil +}