android,libtailscale: implement key.HardwareAttestationKey (#694)
Use a KeyStore-backed key to store a hardware-bound private key. Updates https://github.com/tailscale/tailscale/issues/15830 Signed-off-by: Andrew Lytvynov <awly@tailscale.com>mjf/bumposs
parent
0de26e52c0
commit
11869b00c5
@ -0,0 +1,95 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
package com.tailscale.ipn.util
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import java.security.KeyPair
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.KeyStore
|
||||
import java.security.Signature
|
||||
import kotlin.random.Random
|
||||
|
||||
class NoSuchKeyException : Exception("no key found matching the provided ID")
|
||||
class HardwareKeysNotSupported : Exception("hardware-backed keys are not supported on this device")
|
||||
|
||||
// HardwareKeyStore implements the callbacks necessary to implement key.HardwareAttestationKey on
|
||||
// the Go side. It uses KeyStore with a StrongBox processor.
|
||||
class HardwareKeyStore() {
|
||||
var keyStoreKeys = HashMap<String, KeyPair>();
|
||||
val keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply {
|
||||
load(null)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
fun newID(): String {
|
||||
var id: String
|
||||
do {
|
||||
id = Random.nextBytes(4).toHexString()
|
||||
} while (keyStoreKeys.containsKey(id))
|
||||
return id
|
||||
}
|
||||
|
||||
fun createKey(): String {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
throw HardwareKeysNotSupported()
|
||||
}
|
||||
val id = newID()
|
||||
val kpg: KeyPairGenerator = KeyPairGenerator.getInstance(
|
||||
KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore"
|
||||
)
|
||||
val parameterSpec: KeyGenParameterSpec = KeyGenParameterSpec.Builder(
|
||||
id, KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
|
||||
).run {
|
||||
// Use DIGEST_NONE because hashing is done on the Go side.
|
||||
setDigests(KeyProperties.DIGEST_NONE)
|
||||
setIsStrongBoxBacked(true)
|
||||
build()
|
||||
}
|
||||
|
||||
kpg.initialize(parameterSpec)
|
||||
|
||||
val kp = kpg.generateKeyPair()
|
||||
keyStoreKeys[id] = kp
|
||||
return id
|
||||
}
|
||||
|
||||
fun releaseKey(id: String) {
|
||||
keyStoreKeys.remove(id)
|
||||
}
|
||||
|
||||
fun sign(id: String, data: ByteArray): ByteArray {
|
||||
val key = keyStoreKeys[id]
|
||||
if (key == null) {
|
||||
throw NoSuchKeyException()
|
||||
}
|
||||
// Use NONEwithECDSA because hashing is done on the Go side.
|
||||
return Signature.getInstance("NONEwithECDSA").run {
|
||||
initSign(key.private)
|
||||
update(data)
|
||||
sign()
|
||||
}
|
||||
}
|
||||
|
||||
fun public(id: String): ByteArray {
|
||||
val key = keyStoreKeys[id]
|
||||
if (key == null) {
|
||||
throw NoSuchKeyException()
|
||||
}
|
||||
return key.public.encoded
|
||||
}
|
||||
|
||||
fun load(id: String) {
|
||||
if (keyStoreKeys[id] != null) {
|
||||
// Already loaded.
|
||||
return
|
||||
}
|
||||
val entry: KeyStore.Entry = keyStore.getEntry(id, null)
|
||||
if (entry !is KeyStore.PrivateKeyEntry) {
|
||||
throw NoSuchKeyException()
|
||||
}
|
||||
keyStoreKeys[id] = KeyPair(entry.certificate.publicKey, entry.privateKey)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package libtailscale
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
func emptyHardwareAttestationKey(appCtx AppContext) key.HardwareAttestationKey {
|
||||
return &hardwareAttestationKey{appCtx: appCtx}
|
||||
}
|
||||
|
||||
func createHardwareAttestationKey(appCtx AppContext) (key.HardwareAttestationKey, error) {
|
||||
id, err := appCtx.HardwareAttestationKeyCreate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
k := &hardwareAttestationKey{appCtx: appCtx, id: id}
|
||||
if err := k.fetchPublic(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return k, nil
|
||||
}
|
||||
|
||||
var hardwareAttestationKeyNotInitialized = errors.New("HardwareAttestationKey has not been initialized")
|
||||
|
||||
type hardwareAttestationKey struct {
|
||||
appCtx AppContext
|
||||
id string
|
||||
// public key is always initialized in createHardwareAttestationKey and
|
||||
// UnmarshalJSON. It's only nil in emptyHardwareAttestationKey.
|
||||
public *ecdsa.PublicKey
|
||||
}
|
||||
|
||||
func (k *hardwareAttestationKey) fetchPublic() error {
|
||||
if k.id == "" || k.appCtx == nil {
|
||||
return hardwareAttestationKeyNotInitialized
|
||||
}
|
||||
|
||||
pubRaw, err := k.appCtx.HardwareAttestationKeyPublic(k.id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading public key from KeyStore: %w", err)
|
||||
}
|
||||
pubAny, err := x509.ParsePKIXPublicKey(pubRaw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing public key: %w", err)
|
||||
}
|
||||
pub, ok := pubAny.(*ecdsa.PublicKey)
|
||||
if !ok {
|
||||
return fmt.Errorf("parsed key is %T, expected *ecdsa.PublicKey", pubAny)
|
||||
}
|
||||
k.public = pub
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *hardwareAttestationKey) Public() crypto.PublicKey { return k.public }
|
||||
|
||||
func (k *hardwareAttestationKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) {
|
||||
if k.id == "" || k.appCtx == nil {
|
||||
return nil, hardwareAttestationKeyNotInitialized
|
||||
}
|
||||
return k.appCtx.HardwareAttestationKeySign(k.id, digest)
|
||||
}
|
||||
|
||||
func (k *hardwareAttestationKey) MarshalJSON() ([]byte, error) { return json.Marshal(k.id) }
|
||||
|
||||
func (k *hardwareAttestationKey) UnmarshalJSON(data []byte) error {
|
||||
if err := json.Unmarshal(data, &k.id); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := k.appCtx.HardwareAttestationKeyLoad(k.id); err != nil {
|
||||
return fmt.Errorf("loading key with ID %q from KeyStore: %w", k.id, err)
|
||||
}
|
||||
return k.fetchPublic()
|
||||
}
|
||||
|
||||
func (k *hardwareAttestationKey) Close() error {
|
||||
if k.id == "" || k.appCtx == nil {
|
||||
return hardwareAttestationKeyNotInitialized
|
||||
}
|
||||
return k.appCtx.HardwareAttestationKeyRelease(k.id)
|
||||
}
|
||||
Loading…
Reference in New Issue