Skip to content

Commit

Permalink
Feat: json: Add support for JSON key signature verification
Browse files Browse the repository at this point in the history
  • Loading branch information
zsrv committed Apr 18, 2023
1 parent 5d19ff0 commit 6cba78e
Show file tree
Hide file tree
Showing 8 changed files with 474 additions and 1 deletion.
56 changes: 55 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,36 @@ searching for mac address ...
found match! mac = '3cecef123456'
```

### JSON Keys

Verify the signature of a product key:

```
$ ./supermicro-product-key json verify ac1f6b3ddaec '{"ProductKey":{"Node":{"LicenseID":"1","LicenseName":"SFT-OOB-LIC","CreateDate":"20200921"},"Signature":"OAaLKLy5IEK9WnIdnyA9ew89qTKQrm1eu+Q84CbwjR7XG7JGYccec+3vS3y/kQRRej3DcNVQPWsasX86ROTT+LZFsNY2mIEbQ6+Y/Tmv6+jwYgbQjEN6CjI7ahyKcebN12+3cLvPZyRf3kDqgtcpfuw3Qeg8BbhhyHQk29yNp+NG0XbKn02sHTrskvAGgG0GGlDCT5YmNa0gDSMzsvt/eH9nskb5opQNE3j7MAMXbjpI7xVHRbmB2N5iSu8gQUj0/pmk615ztM/uB54ur3GninJRU74S9Kotz+JunJg4pprGyQW544ggmzklmtr3zCA3GK/d929eZsVk5p8UxXG7wQ=="}}'
signature verified ok
```

Find the MAC address associated with a key (this can take a long time for JSON keys - up to one hour):

```
$ ./supermicro-product-key json bruteforce '{"ProductKey":{"Node":{"LicenseID":"1","LicenseName":"SFT-OOB-LIC","CreateDate":"20200921"},"Signature":"OAaLKLy5IEK9WnIdnyA9ew89qTKQrm1eu+Q84CbwjR7XG7JGYccec+3vS3y/kQRRej3DcNVQPWsasX86ROTT+LZFsNY2mIEbQ6+Y/Tmv6+jwYgbQjEN6CjI7ahyKcebN12+3cLvPZyRf3kDqgtcpfuw3Qeg8BbhhyHQk29yNp+NG0XbKn02sHTrskvAGgG0GGlDCT5YmNa0gDSMzsvt/eH9nskb5opQNE3j7MAMXbjpI7xVHRbmB2N5iSu8gQUj0/pmk615ztM/uB54ur3GninJRU74S9Kotz+JunJg4pprGyQW544ggmzklmtr3zCA3GK/d929eZsVk5p8UxXG7wQ=="}}'
searching for mac address ...
found match! mac = 'ac1f6b3ddaec'
```

List all key types that are available:

```
$ ./supermicro-product-key json listswid
License SKU ID
----------- --
SFT-OOB-LIC 1
SFT-DCMS-SINGLE 2
SFT-SPM-LIC 3
SFT-DCMS-SVC-KEY 4
SFT-SDDC-SINGLE 5
```

## Product Key Formats and SKUs

This section describes each product key format, and each license SKU
Expand Down Expand Up @@ -322,7 +352,31 @@ A variable-length JSON string.
The contents are digitally signed, and the signatures verified using a public key that is
embedded in the BMC firmware.

This key format is not currently supported by this utility.
This key format can only be verified by this utility, not generated.

#### SFT-OOB-LIC

##### Samples

```
Source: https://store.supermicro.com/software/dcms-key-activation-guide
MAC Address: ac1f6b3ddaec (different from what is displayed in the guide!)
Key: {"ProductKey":{"Node":{"LicenseID":"1","LicenseName":"SFT-OOB-LIC","CreateDate":"20200921"},"Signature":"OAaLKLy5IEK9WnIdnyA9ew89qTKQrm1eu+Q84CbwjR7XG7JGYccec+3vS3y/kQRRej3DcNVQPWsasX86ROTT+LZFsNY2mIEbQ6+Y/Tmv6+jwYgbQjEN6CjI7ahyKcebN12+3cLvPZyRf3kDqgtcpfuw3Qeg8BbhhyHQk29yNp+NG0XbKn02sHTrskvAGgG0GGlDCT5YmNa0gDSMzsvt/eH9nskb5opQNE3j7MAMXbjpI7xVHRbmB2N5iSu8gQUj0/pmk615ztM/uB54ur3GninJRU74S9Kotz+JunJg4pprGyQW544ggmzklmtr3zCA3GK/d929eZsVk5p8UxXG7wQ=="}}
```

#### SFT-DCMS-SINGLE

##### Samples

```
Source: https://github.com/supermicro/redfish/blob/ea5ea99eda0b7ac50d48ca7faa307dfcd3f41e05/Postman_Collections/05_managers.postman_collection.json#L1348
MAC Address: 3cecef72fc46
Key: {"ProductKey":{"Node":{"LicenseID":"2","LicenseName":"SFT-DCMS-SINGLE","CreateDate":"20220614"},"Signature":"ZKFCkgKEYh9+8MNZW7RfPlt/nRxQJGJ0kLHLkalLt1tpgs4MTLHrXvp/eZzfhSPUb5qMNu9RkFn9MaukK6vNXlOIG7ijbR+vjkxVcdIIkMnhzHFLxE/0ws74/lJyGLkSO1jHRQRaczSDuHgzSgsWivjHejB/tRlSpnAEM7FplgyuBSbisek8pEgSKua5jCf7Zn4sjYXXO7T9rTV4aFq090XgRbEay45eBSGpun9pcyGs8UIeNH93qzqCmlkcjj+bFSNcm3VeucEjScE3fzqG93NMEQQWYEdsYcuJb4a+kWP/ffFvyVRWvqSWvPgD5N+eNqKAmmC4MmjykRy3DWw4fA=="}}
```

## Platform Support

Expand Down
73 changes: 73 additions & 0 deletions cmd/json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package cmd

import (
"fmt"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/zsrv/supermicro-product-key/pkg/json"
"os"
"text/tabwriter"
)

func init() {
rootCmd.AddCommand(jsonCmd)
jsonCmd.AddCommand(jsonVerifyCmd)
jsonCmd.AddCommand(jsonBruteForceCmd)
jsonCmd.AddCommand(jsonListSKUCmd)
}

var jsonCmd = &cobra.Command{
Use: "json",
Short: "JSON product key operations",
}

var jsonVerifyCmd = &cobra.Command{
Use: "verify MAC_ADDRESS PRODUCT_KEY",
Short: "Verify the signature of a JSON product key",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
macAddress := args[0]
productKey := args[1]

if err := json.VerifyProductKeySignature(productKey, macAddress); err != nil {
return errors.WithMessage(err, "product key verification failed")
}

fmt.Println("signature verified ok")
return nil
},
}

var jsonBruteForceCmd = &cobra.Command{
Use: "bruteforce PRODUCT_KEY",
Short: "Find the MAC address associated with a JSON product key by brute force",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
productKey := args[0]

fmt.Println("searching for mac address ...")

mac, err := json.BruteForceMACAddressFromString(productKey)
if err != nil {
return err
}

fmt.Printf("found match! mac = '%s'\n", mac)
return nil
},
}

var jsonListSKUCmd = &cobra.Command{
Use: "listswid",
Short: "Get a list of software identifiers that can be found in product keys",
Args: cobra.ExactArgs(0),
Run: func(cmd *cobra.Command, args []string) {
w := tabwriter.NewWriter(os.Stdout, 3, 1, 2, ' ', 0)
fmt.Fprintf(w, "License SKU\tID\n")
fmt.Fprintf(w, "-----------\t--\n")
for _, swid := range json.SoftwareIdentifiers.List() {
fmt.Fprintf(w, "%v\t%v\n", swid.SKU, swid.ID)
}
w.Flush()
},
}
65 changes: 65 additions & 0 deletions pkg/json/brute_force.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package json

import (
"encoding/hex"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/zsrv/supermicro-product-key/pkg/oob"
"strings"
)

// BruteForceMACAddressFromString finds the MAC address associated with the productKey.
func BruteForceMACAddressFromString(productKey string) (string, error) {
license, err := unmarshalLicenseFromJSON(productKey)
if err != nil {
return "", err
}

return BruteForceMACAddress(license)
}

// BruteForceMACAddress finds the MAC address associated with the license.
func BruteForceMACAddress(license License) (string, error) {
brute := func(macBlock string, result chan string, done chan bool) {
log.Debug().Msgf("searching mac address block '%s'", macBlock)

for one := 0; one <= 255; one++ {
hexOne := hex.EncodeToString([]byte{byte(one)})
for two := 0; two <= 255; two++ {
hexTwo := hex.EncodeToString([]byte{byte(two)})
for three := 0; three <= 255; three++ {
hexThree := hex.EncodeToString([]byte{byte(three)})

mac := strings.ToUpper(macBlock + hexOne + hexTwo + hexThree)
if err := license.VerifySignature(mac); err != nil {
continue
}

result <- mac
done <- true
}
}
}

log.Debug().Msgf("finished searching mac address block '%s' with no matches", macBlock)
done <- true
}

result := make(chan string)
done := make(chan bool)

for _, macBlock := range oob.SupermicroMACAddressBlocks {
go brute(macBlock, result, done)
}

for range oob.SupermicroMACAddressBlocks {
select {
case resultMAC := <-result:
return strings.ToLower(resultMAC), nil
case <-done:
continue
}
}

return "", errors.New("could not find a matching mac address")
}
38 changes: 38 additions & 0 deletions pkg/json/brute_force_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package json

import (
"testing"
)

func TestBruteForceMACAddressFromString(t *testing.T) {
type args struct {
productKey string
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "valid",
args: args{
productKey: validOOBProductKey,
},
want: validOOBProductKeyMACAddress,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := BruteForceMACAddressFromString(tt.args.productKey)
if (err != nil) != tt.wantErr {
t.Errorf("BruteForceMACAddressFromString() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("BruteForceMACAddressFromString() got = %v, want %v", got, tt.want)
}
})
}
}
43 changes: 43 additions & 0 deletions pkg/json/json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package json

import (
"crypto/rsa"
"crypto/x509"
"encoding/json"
"encoding/pem"
"github.com/pkg/errors"
)

func VerifyProductKeySignature(productKey string, macAddress string) error {
license, err := unmarshalLicenseFromJSON(productKey)
if err != nil {
return err
}

return license.VerifySignature(macAddress)
}

func unmarshalLicenseFromJSON(productKey string) (License, error) {
var license License
err := json.Unmarshal([]byte(productKey), &license)
if err != nil {
return License{}, errors.WithMessage(err, "could not unmarshal license JSON")
}

return license, nil
}

// parseRSAPublicKey returns a rsa.PublicKey parsed from pemData.
func parseRSAPublicKey(pemData []byte) (*rsa.PublicKey, error) {
block, _ := pem.Decode(pemData)
if block == nil || block.Type != "RSA PUBLIC KEY" {
return nil, errors.New("failed to decode PEM block containing public key")
}

pub, err := x509.ParsePKCS1PublicKey(block.Bytes)
if err != nil {
return nil, err
}

return pub, nil
}
34 changes: 34 additions & 0 deletions pkg/json/json_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package json

import "testing"

var validOOBProductKey = `{"ProductKey":{"Node":{"LicenseID":"1","LicenseName":"SFT-OOB-LIC","CreateDate":"20200921"},"Signature":"OAaLKLy5IEK9WnIdnyA9ew89qTKQrm1eu+Q84CbwjR7XG7JGYccec+3vS3y/kQRRej3DcNVQPWsasX86ROTT+LZFsNY2mIEbQ6+Y/Tmv6+jwYgbQjEN6CjI7ahyKcebN12+3cLvPZyRf3kDqgtcpfuw3Qeg8BbhhyHQk29yNp+NG0XbKn02sHTrskvAGgG0GGlDCT5YmNa0gDSMzsvt/eH9nskb5opQNE3j7MAMXbjpI7xVHRbmB2N5iSu8gQUj0/pmk615ztM/uB54ur3GninJRU74S9Kotz+JunJg4pprGyQW544ggmzklmtr3zCA3GK/d929eZsVk5p8UxXG7wQ=="}}`
var validOOBProductKeyMACAddress = "ac1f6b3ddaec"

func TestVerifyProductKeySignature(t *testing.T) {
type args struct {
productKey string
macAddress string
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "valid",
args: args{
productKey: validOOBProductKey,
macAddress: validOOBProductKeyMACAddress,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := VerifyProductKeySignature(tt.args.productKey, tt.args.macAddress); (err != nil) != tt.wantErr {
t.Errorf("VerifyProductKeySignature() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
70 changes: 70 additions & 0 deletions pkg/json/product_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package json

import (
"crypto"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"github.com/zsrv/supermicro-product-key/pkg/oob"
"strings"
)

// licenseSigningPublicKey is used to validate JSON format licenses
// that have been digitally signed by Supermicro.
var licenseSigningPublicKey = []byte(`-----BEGIN RSA PUBLIC KEY-----
MIIBCAKCAQEAvDb4MxWw/FTi8pscP6S2YAl/3gmVOj/StG0lu3PdCSmdpzmzbOU9
KBS3t0yPZ0ynUQj/qXOwaVLvBJ+uCE0pGIRWkzBersVUzmXXN8Dza5yOzlLIdsVn
amUrKcRHgC+otRE/gnCxIiioacy9TkA96otbAvztCl1j1W8oCixazpfwZrayy12y
CcOyquZr3prngLCgOWa9e9cLSekKuvYXPKPC0CogLz0ueg+y+gcWGHGwbMtERLGf
WYDrXD1mdlV0EL4A5H4v9bqzn0yHe9dIb7+tdMHiN+qLjMzcVSzwfXu7Abk7rPOz
wBY6gMvfhfaOTj8aU5JFQ3DEBdTPOwgtSwIBAw==
-----END RSA PUBLIC KEY-----`)

type License struct {
ProductKey ProductKey
}

type ProductKey struct {
Node Node
Signature string
}

type Node struct {
LicenseID string
LicenseName string
CreateDate string
}

// VerifySignature returns nil if the license signature is valid when the given
// macAddress is used as input in the message being hashed and signed, or an error
// if one occurs.
func (l *License) VerifySignature(macAddress string) error {
macAddress, err := oob.NormalizeMACAddress(macAddress)
if err != nil {
return err
}
macAddress = strings.ToUpper(macAddress)

publicKey, err := parseRSAPublicKey(licenseSigningPublicKey)
if err != nil {
return err
}

signature, err := base64.StdEncoding.DecodeString(l.ProductKey.Signature)
if err != nil {
return err
}

message := []byte(macAddress +
l.ProductKey.Node.LicenseID +
l.ProductKey.Node.LicenseName +
l.ProductKey.Node.CreateDate)
hashed := sha256.Sum256(message)

err = rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, hashed[:], signature)
if err != nil {
return err
}

return nil
}
Loading

0 comments on commit 6cba78e

Please sign in to comment.