android,libtailscale: implement key.HardwareAttestationKey
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>pull/694/head
parent
0de26e52c0
commit
a78c07ae2f
@ -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