Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 87 additions & 52 deletions api/firmware/attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"crypto/elliptic"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"math/big"

Expand Down Expand Up @@ -134,6 +135,8 @@ var attestationPubkeys = []string{
// The identifier is sha256(pubkey).
var attestationPubkeysMap map[string]string

const attestationPayloadLength = 32 + 64 + 64 + 32 + 64

func unhex(s string) []byte {
b, err := hex.DecodeString(s)
if err != nil {
Expand All @@ -151,82 +154,114 @@ func init() {
}
}

// performAttestation sends a random challenge and verifies that the response can be verified with
// Shift's root attestation pubkeys. Returns true if the verification is successful.
func (device *Device) performAttestation() (bool, error) {
if !device.version.AtLeast(semver.NewSemVer(2, 0, 0)) {
// skip warning for v1.0.0, where attestation was not supported.
return true, nil
}
challenge := bytesOrPanic(32)
response, err := device.rawQuery(append([]byte(opAttestation), challenge...))
if err != nil {
device.log.Error(fmt.Sprintf("attestation: could not perform request. challenge=%x", challenge), err)
return false, err
}
func verifyECDSASignature(pubkey *ecdsa.PublicKey, message []byte, signature []byte) bool {
sigR := new(big.Int).SetBytes(signature[:32])
sigS := new(big.Int).SetBytes(signature[32:])
sigHash := sha256.Sum256(message)
return ecdsa.Verify(pubkey, sigHash[:], sigR, sigS)
}

// See parsing below for what the sizes mean.
if len(response) < 1+32+64+64+32+64 {
device.log.Error(
fmt.Sprintf("attestation: response too short. challenge=%x, response=%x", challenge, response), nil)
return false, nil
}
if string(response[:1]) != responseSuccess {
device.log.Error(
fmt.Sprintf("attestation: expected success. challenge=%x, response=%x", challenge, response), nil)
return false, nil
type invalidAttestationError struct {
message string
}

func (err invalidAttestationError) Error() string {
return err.message
}

// VerifyAttestation verifies the 256-byte attestation payload returned by the device, excluding the
// first success byte, against Shift's root attestation pubkeys and the original challenge.
func VerifyAttestation(challenge []byte, attestation []byte) error {
if len(attestation) != attestationPayloadLength {
return errp.Newf(
"attestation must be %d bytes, got %d", attestationPayloadLength, len(attestation))
}
rsp := response[1:]

var bootloaderHash, devicePubkeyBytes, certificate, rootPubkeyIdentifier, challengeSignature []byte
bootloaderHash, rsp = rsp[:32], rsp[32:]
devicePubkeyBytes, rsp = rsp[:64], rsp[64:]
certificate, rsp = rsp[:64], rsp[64:]
rootPubkeyIdentifier, rsp = rsp[:32], rsp[32:]
challengeSignature = rsp[:64]
bootloaderHash, attestation = attestation[:32], attestation[32:]
devicePubkeyBytes, attestation = attestation[:64], attestation[64:]
certificate, attestation = attestation[:64], attestation[64:]
rootPubkeyIdentifier, attestation = attestation[:32], attestation[32:]
challengeSignature = attestation[:64]

pubkeyHex, ok := attestationPubkeysMap[hex.EncodeToString(rootPubkeyIdentifier)]
if !ok {
device.log.Error(fmt.Sprintf(
"could not find root pubkey. challenge=%x, response=%x, identifier=%x",
challenge,
response,
rootPubkeyIdentifier), nil)
return false, nil
return errp.Newf("could not find root pubkey. identifier=%x", rootPubkeyIdentifier)
}

rootPubkeyBytes, err := hex.DecodeString(pubkeyHex)
if err != nil {
panic(errp.WithStack(err))
return errp.WithStack(err)
}
rootPubkey, err := btcec.ParsePubKey(rootPubkeyBytes)
if err != nil {
panic(errp.WithStack(err))
return errp.WithStack(err)
}

devicePubkey := ecdsa.PublicKey{
Curve: elliptic.P256(),
X: new(big.Int).SetBytes(devicePubkeyBytes[:32]),
Y: new(big.Int).SetBytes(devicePubkeyBytes[32:]),
}

verify := func(pubkey *ecdsa.PublicKey, message []byte, signature []byte) bool {
sigR := new(big.Int).SetBytes(signature[:32])
sigS := new(big.Int).SetBytes(signature[32:])
sigHash := sha256.Sum256(message)
return ecdsa.Verify(pubkey, sigHash[:], sigR, sigS)
}

// Verify certificate
var certMsg bytes.Buffer
certMsg.Write(bootloaderHash)
certMsg.Write(devicePubkeyBytes)
if !verify(rootPubkey.ToECDSA(), certMsg.Bytes(), certificate) {
device.log.Error(
fmt.Sprintf("attestation: could not verify certificate. challenge=%x, response=%x", challenge, response), nil)
return false, nil
if !verifyECDSASignature(rootPubkey.ToECDSA(), certMsg.Bytes(), certificate) {
return errp.New("could not verify certificate")
}
if !verifyECDSASignature(&devicePubkey, challenge, challengeSignature) {
return errp.New("could not verify challenge signature")
}

return nil
}

// GetAttestation sends challenge to the device and returns the 256-byte attestation payload,
// excluding the first success byte.
func (device *Device) GetAttestation(challenge []byte) ([]byte, error) {
if !device.version.AtLeast(semver.NewSemVer(2, 0, 0)) {
return nil, errp.New("attestation not supported")
}

response, err := device.rawQuery(append([]byte(opAttestation), challenge...))
if err != nil {
return nil, err
}

if len(response) < 1+attestationPayloadLength {
return nil, invalidAttestationError{message: "response too short"}
}
if string(response[:1]) != responseSuccess {
return nil, invalidAttestationError{message: "expected success"}
}

return response[1 : 1+attestationPayloadLength], nil
}

// performAttestation sends a random challenge and verifies that the response can be verified with
// Shift's root attestation pubkeys. Returns true if the verification is successful.
func (device *Device) performAttestation() (bool, error) {
if !device.version.AtLeast(semver.NewSemVer(2, 0, 0)) {
// skip warning for v1.0.0, where attestation was not supported.
return true, nil
}
challenge := bytesOrPanic(32)
attestation, err := device.GetAttestation(challenge)
if err != nil {
var invalidErr invalidAttestationError
if errors.As(err, &invalidErr) {
device.log.Error(fmt.Sprintf("attestation: %v. challenge=%x", err, challenge), nil)
return false, nil
}
device.log.Error(fmt.Sprintf("attestation: could not perform request. challenge=%x", challenge), err)
return false, err
}
// Verify challenge
if !verify(&devicePubkey, challenge, challengeSignature) {

err = VerifyAttestation(challenge, attestation)
if err != nil {
device.log.Error(
fmt.Sprintf("attestation: could not verify challgege signature. challenge=%x, response=%x", challenge, response), nil)
fmt.Sprintf("attestation: %v. challenge=%x, attestation=%x", err, challenge, attestation), nil)
return false, nil
}
return true, nil
Expand Down
147 changes: 145 additions & 2 deletions api/firmware/attestation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func p256PrivKeyFromBytes(k []byte) *ecdsa.PrivateKey {
return priv
}

func TestAttestation(t *testing.T) {
func TestPerformAttestation(t *testing.T) {

// Arbitrary values, they do not have any special meaning.
// identifier is the sha256 hash of the uncompressed pubkey.
Expand Down Expand Up @@ -100,7 +100,7 @@ func TestAttestation(t *testing.T) {

// Invalid response status code.
communication.MockQuery = func([]byte) ([]byte, error) {
response := make([]byte, 1+32+64+64+32+64)
response := make([]byte, 1+attestationPayloadLength)
response[0] = 0x01
return response, nil
}
Expand Down Expand Up @@ -192,3 +192,146 @@ func TestAttestation(t *testing.T) {
require.NoError(t, err)
require.True(t, success)
}

func TestVerifyAttestation(t *testing.T) {
// Arbitrary values, they do not have any special meaning.
// identifier is the sha256 hash of the uncompressed pubkey.
challenge := unhex("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff")
rootPubkeyIdentifier := unhex("11554d841e74066eebc3556ed6dea4d6ceef3940009222c77c3b966349989de1")
rootPrivateKey, rootPublicKey := btcec.PrivKeyFromBytes(
unhex("15608dfed8e876bed1cf2599574ce853f7a2a017d19ba0aabd4bcba033a70880"),
)
bootloaderHash := unhex("3fdf2ff2dcbd31d161a525a88cb57641209c7eac2bc014564a03d34a825144f0")
devicePrivateKey := p256PrivKeyFromBytes(
unhex("9b1a4d293a6eef1960d8afab5e58dd581b135152ec3399bde9268fa23051321b"),
)
devicePublicKey := devicePrivateKey.PublicKey
devicePubkeyBytes := make([]byte, 64)
copy(devicePubkeyBytes[:32], devicePublicKey.X.Bytes())
copy(devicePubkeyBytes[32:], devicePublicKey.Y.Bytes())
certificate := makeCertificate(rootPrivateKey, bootloaderHash, devicePubkeyBytes)

undo := addAttestationPubkey(hex.EncodeToString(rootPublicKey.SerializeUncompressed()))
defer undo()

makeAttestation := func(
certificate []byte,
rootPubkeyIdentifier []byte,
challengeSignature []byte,
) []byte {
var buf bytes.Buffer
buf.Write(bootloaderHash)
buf.Write(devicePubkeyBytes)
buf.Write(certificate)
buf.Write(rootPubkeyIdentifier)
buf.Write(challengeSignature)
return buf.Bytes()
}
makeChallengeSignature := func(challenge []byte) []byte {
sigHash := sha256.Sum256(challenge)
sigR, sigS, err := ecdsa.Sign(rand.Reader, devicePrivateKey, sigHash[:])
if err != nil {
panic(err)
}
signature := make([]byte, 64)
sigR.FillBytes(signature[:32])
sigS.FillBytes(signature[32:])
return signature
}

err := VerifyAttestation(challenge, nil)
require.EqualError(t, err, "attestation must be 256 bytes, got 0")

err = VerifyAttestation(
challenge,
makeAttestation(
make([]byte, 64),
make([]byte, 32),
make([]byte, 64),
),
)
require.EqualError(t, err, "could not find root pubkey. identifier=0000000000000000000000000000000000000000000000000000000000000000")

err = VerifyAttestation(
challenge,
makeAttestation(
make([]byte, 64),
rootPubkeyIdentifier,
make([]byte, 64),
),
)
require.EqualError(t, err, "could not verify certificate")

err = VerifyAttestation(
challenge,
makeAttestation(
certificate,
rootPubkeyIdentifier,
make([]byte, 64),
),
)
require.EqualError(t, err, "could not verify challenge signature")

err = VerifyAttestation(
challenge,
makeAttestation(
certificate,
rootPubkeyIdentifier,
makeChallengeSignature(challenge),
),
)
require.NoError(t, err)
}

func TestGetAttestation(t *testing.T) {
challenge := unhex("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff")
communication := &mocks.Communication{}
product := common.ProductBitBox02BTCOnly

device := NewDevice(
semver.NewSemVer(1, 0, 0),
&product,
&mocks.Config{}, communication, &mocks.Logger{},
)
attestation, err := device.GetAttestation(challenge)
require.EqualError(t, err, "attestation not supported")
require.Nil(t, attestation)

device = NewDevice(
semver.NewSemVer(2, 0, 0),
&product,
&mocks.Config{}, communication, &mocks.Logger{},
)

expectedErr := errors.New("error")
communication.MockQuery = func([]byte) ([]byte, error) {
return nil, expectedErr
}
attestation, err = device.GetAttestation(challenge)
require.Equal(t, expectedErr, err)
require.Nil(t, attestation)

communication.MockQuery = func([]byte) ([]byte, error) {
return nil, nil
}
attestation, err = device.GetAttestation(challenge)
require.EqualError(t, err, "response too short")
require.Nil(t, attestation)

communication.MockQuery = func([]byte) ([]byte, error) {
response := make([]byte, 1+attestationPayloadLength)
response[0] = 0x01
return response, nil
}
attestation, err = device.GetAttestation(challenge)
require.EqualError(t, err, "expected success")
require.Nil(t, attestation)

expectedAttestation := bytes.Repeat([]byte{0x42}, attestationPayloadLength)
communication.MockQuery = func([]byte) ([]byte, error) {
return append([]byte{0x00}, expectedAttestation...), nil
}
attestation, err = device.GetAttestation(challenge)
require.NoError(t, err)
require.Equal(t, expectedAttestation, attestation)
}
Loading