From c8370d8e1ea5c10766149ba4aa84a99d58a4bcef Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Wed, 11 Mar 2026 12:54:45 +0000 Subject: [PATCH] attestation: refactor to expose verification function Expose expose verification and GetAttestation. Could be useful for BitBoxCloud for the server to verify attestations, and the client to perform attestation proofs for the server. --- api/firmware/attestation.go | 139 ++++++++++++++++++----------- api/firmware/attestation_test.go | 147 ++++++++++++++++++++++++++++++- 2 files changed, 232 insertions(+), 54 deletions(-) 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) +}