diff --git a/api/firmware/attestation.go b/api/firmware/attestation.go index fe3f2e7..a398456 100644 --- a/api/firmware/attestation.go +++ b/api/firmware/attestation.go @@ -8,6 +8,7 @@ import ( "crypto/elliptic" "crypto/sha256" "encoding/hex" + "errors" "fmt" "math/big" @@ -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 { @@ -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 diff --git a/api/firmware/attestation_test.go b/api/firmware/attestation_test.go index 124c89e..19ffcc4 100644 --- a/api/firmware/attestation_test.go +++ b/api/firmware/attestation_test.go @@ -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. @@ -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 } @@ -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) +}