CTAP library move (#602)
* Moves all CTAP logic into its own library * workflows fix test * more coveralls workflow tests
This commit is contained in:
50
libraries/opensk/Cargo.toml
Normal file
50
libraries/opensk/Cargo.toml
Normal file
@@ -0,0 +1,50 @@
|
||||
[package]
|
||||
name = "opensk"
|
||||
version = "1.0.0"
|
||||
authors = [
|
||||
"Fabian Kaczmarczyck <kaczmarczyck@google.com>",
|
||||
"Guillaume Endignoux <guillaumee@google.com>",
|
||||
"Jean-Michel Picod <jmichel@google.com>",
|
||||
"Julien Cretin <cretin@google.com>",
|
||||
]
|
||||
license = "Apache-2.0"
|
||||
edition = "2018"
|
||||
rust-version = "1.47"
|
||||
|
||||
[dependencies]
|
||||
sk-cbor = { path = "../cbor" }
|
||||
crypto = { path = "../crypto" }
|
||||
rng256 = { path = "../rng256" }
|
||||
persistent_store = { path = "../persistent_store" }
|
||||
byteorder = { version = "1", default-features = false }
|
||||
arrayref = "0.3.6"
|
||||
subtle = { version = "2.2", default-features = false, features = ["nightly"] }
|
||||
arbitrary = { version = "0.4.7", features = ["derive"], optional = true }
|
||||
rand = { version = "0.8.4", optional = true }
|
||||
ed25519-compact = { version = "1", default-features = false, optional = true }
|
||||
|
||||
[features]
|
||||
debug_ctap = []
|
||||
std = ["crypto/std", "persistent_store/std", "rng256/std", "rand"]
|
||||
with_ctap1 = ["crypto/with_ctap1"]
|
||||
vendor_hid = []
|
||||
fuzz = ["arbitrary", "std"]
|
||||
ed25519 = ["ed25519-compact"]
|
||||
|
||||
[dev-dependencies]
|
||||
enum-iterator = "0.6.0"
|
||||
|
||||
[build-dependencies]
|
||||
sk-cbor = { path = "../cbor" }
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
||||
openssl = "0.10.36"
|
||||
|
||||
[profile.dev]
|
||||
panic = "abort"
|
||||
lto = true # Link Time Optimization usually reduces size of binaries and static libraries
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
lto = true # Link Time Optimization usually reduces size of binaries and static libraries
|
||||
opt-level = "z"
|
||||
codegen-units = 1
|
||||
3
libraries/opensk/fuzz/.gitignore
vendored
Normal file
3
libraries/opensk/fuzz/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/artifacts/
|
||||
/corpus/
|
||||
/target/
|
||||
71
libraries/opensk/fuzz/Cargo.toml
Normal file
71
libraries/opensk/fuzz/Cargo.toml
Normal file
@@ -0,0 +1,71 @@
|
||||
[package]
|
||||
name = "opensk-fuzz"
|
||||
version = "0.0.0"
|
||||
authors = ["Automatically generated"]
|
||||
publish = false
|
||||
edition = "2018"
|
||||
|
||||
[package.metadata]
|
||||
cargo-fuzz = true
|
||||
|
||||
[dependencies]
|
||||
libfuzzer-sys = { version = "0.3" }
|
||||
fuzz_helper = { path = "fuzz_helper" }
|
||||
|
||||
# Prevent this from interfering with workspaces
|
||||
[workspace]
|
||||
members = ["."]
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_target_process_ctap_command"
|
||||
path = "fuzz_targets/fuzz_target_process_ctap_command.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_target_process_ctap1"
|
||||
path = "fuzz_targets/fuzz_target_process_ctap1.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_target_process_ctap2_client_pin"
|
||||
path = "fuzz_targets/fuzz_target_process_ctap2_client_pin.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_target_process_ctap2_client_pin_structured"
|
||||
path = "fuzz_targets/fuzz_target_process_ctap2_client_pin_structured.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_target_process_ctap2_get_assertion"
|
||||
path = "fuzz_targets/fuzz_target_process_ctap2_get_assertion.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_target_process_ctap2_get_assertion_structured"
|
||||
path = "fuzz_targets/fuzz_target_process_ctap2_get_assertion_structured.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_target_process_ctap2_make_credential"
|
||||
path = "fuzz_targets/fuzz_target_process_ctap2_make_credential.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_target_process_ctap2_make_credential_structured"
|
||||
path = "fuzz_targets/fuzz_target_process_ctap2_make_credential_structured.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_target_split_assemble"
|
||||
path = "fuzz_targets/fuzz_target_split_assemble.rs"
|
||||
test = false
|
||||
doc = false
|
||||
152
libraries/opensk/fuzz/ctap2_commands_parameters_corpus.json
Normal file
152
libraries/opensk/fuzz/ctap2_commands_parameters_corpus.json
Normal file
@@ -0,0 +1,152 @@
|
||||
[
|
||||
{
|
||||
"hex": "1903e8",
|
||||
"cbor": "unsigned(1000)",
|
||||
"description": "cbor value"
|
||||
},
|
||||
{
|
||||
"hex": "3829",
|
||||
"cbor": "negative(41)",
|
||||
"description": "cbor value"
|
||||
},
|
||||
{
|
||||
"hex": "c349010000000000000000",
|
||||
"cbor": "-18446744073709551617",
|
||||
"description": "cbor value"
|
||||
},
|
||||
{
|
||||
"hex": "f90000",
|
||||
"cbor": "primitive(0)",
|
||||
"description": "cbor value"
|
||||
},
|
||||
{
|
||||
"hex": "f90001",
|
||||
"cbor": "primitive(1) = 5.960464477539063e-8",
|
||||
"description": "cbor value"
|
||||
},
|
||||
{
|
||||
"hex": "fa7fc00000",
|
||||
"cbor": "primitive(2143289344) = NaN",
|
||||
"description": "cbor value"
|
||||
},
|
||||
{
|
||||
"hex": "f818",
|
||||
"cbor": "simple(24)",
|
||||
"description": "cbor value"
|
||||
},
|
||||
{
|
||||
"hex": "d74401020304",
|
||||
"cbor": "tag 23(h'01020304')",
|
||||
"description": "cbor value"
|
||||
},
|
||||
{
|
||||
"hex": "6449455446",
|
||||
"cbor": "IETF",
|
||||
"description": "cbor value"
|
||||
},
|
||||
{
|
||||
"hex": "62225c",
|
||||
"cbor": "\"\\",
|
||||
"description": "cbor value"
|
||||
},
|
||||
{
|
||||
"hex": "41a8",
|
||||
"cbor": "bytes(a8)",
|
||||
"description": "cbor value"
|
||||
},
|
||||
{
|
||||
"hex": "623a41",
|
||||
"cbor": "text(:A)",
|
||||
"description": "cbor value"
|
||||
},
|
||||
{
|
||||
"hex": "83019f0203ff820405",
|
||||
"cbor": "array [1, [2, 3], [4, 5]]",
|
||||
"description": "cbor value"
|
||||
},
|
||||
{
|
||||
"hex": "9f018202039f0405ffff",
|
||||
"cbor": "indefinite length array [1, [2, 3], [4, 5]]",
|
||||
"description": "cbor value"
|
||||
},
|
||||
{
|
||||
"hex": "5f44aabbccdd43eeff99ff",
|
||||
"cbor": "indefinite byte string (_ h'AABBCCDD', h'EEFF99')",
|
||||
"description": "cbor value"
|
||||
},
|
||||
{
|
||||
"hex": "7f657374726561646d696e67ff",
|
||||
"cbor": "indefinite byte string (_ \"strea\", \"ming\")",
|
||||
"description": "cbor value"
|
||||
},
|
||||
{
|
||||
"hex": "a26161016162820203",
|
||||
"cbor": "map {\"a\": 1, \"b\": [2, 3]}",
|
||||
"description": "cbor value"
|
||||
},
|
||||
{
|
||||
"hex": "bf6346756ef563416d7421ff",
|
||||
"cbor": "indefinite length map {\"Fun\": true, \"Amt\": -2}",
|
||||
"description": "cbor value"
|
||||
},
|
||||
{
|
||||
"hex": "a4015820cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd02a14269645770696E5F616273656E63652E6578616D706C652E636F6D03a262696458201D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D646e616d65644164616d0481a263616c672664747970656a7075626C69632D6B6579",
|
||||
"cbor": "{1: h'CDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCD', 2: {\"id\": \"pin_absence.example.com\"}, 3: {\"id\": h'1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D', \"name\": \"Adam\"}, 4: [{\"alg\": -7, \"type\": \"public-key\"}]}",
|
||||
"description": "make credential parameter 1"
|
||||
},
|
||||
{
|
||||
"hex": "a9015820687134968222ec17202e42505f8ed2b16ae22f16bb05b88c25db9e602645f14102a3626964781a6d616b655f6261645f74797065732e6578616d706c652e636f6d6469636f6e6f687474703a2f2f69636f6e2e706e67646e616d65676578616d706c6503a462696458201d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d6469636f6e6f687474703a2f2f69636f6e2e706e67646e616d65684a6f686e20446f656b646973706c61794e616d65624a440481a263616c672664747970656a7075626c69632d6b65790581a26269644064747970656a7075626c69632d6b657906a007a362726bf4627570f5627576f40850610c58506c864a708e26dd0ccf4be3d90901",
|
||||
"cbor": "{1: h'687134968222EC17202E42505F8ED2B16AE22F16BB05B88C25DB9E602645F141', 2: {\"id\": \"make_bad_types.example.com\", \"icon\": \"http://icon.png\", \"name\": \"example\"}, 3: {\"id\": h'1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D', \"icon\": \"http://icon.png\", \"name\": \"John Doe\", \"displayName\": \"JD\"}, 4: [{\"alg\": -7, \"type\": \"public-key\"}], 5: [{\"id\": h'', \"type\": \"public-key\"}], 6: {}, 7: {\"rk\": false, \"up\": true, \"uv\": false}, 8: h'610C58506C864A708E26DD0CCF4BE3D9', 9: 1}",
|
||||
"description": "make credential parameters 2"
|
||||
},
|
||||
{
|
||||
"hex": "a9015820cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd02a3626964781a6d616b655f6261645f74797065732e6578616d706c652e636f6d6469636f6e6f687474703a2f2f69636f6e2e706e67646e616d65646A6F686E03a462696458201d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d6469636f6e6f687474703a2f2f69636f6e2e706e67646e616d65684a6f686e20446f656b646973706c61794e616d65624a440481a263616c672664747970656a7075626c69632d6b65790581a26269644064747970656a7075626c69632d6b657906a007a362726bf4627570f5627576f40850610c58506c864a708e26dd0ccf4be3d90901",
|
||||
"cbor": "{1: h'CDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCD', 2: {\"id\": \"make_bad_types.example.com\", \"icon\": \"http://icon.png\", \"name\": \"john\"}, 3: {\"id\": h'1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D', \"icon\": \"http://icon.png\", \"name\": \"John Doe\", \"displayName\": \"JD\"}, 4: [{\"alg\": -7, \"type\": \"public-key\"}], 5: [{\"id\": h'', \"type\": \"public-key\"}], 6: {}, 7: {\"rk\": false, \"up\": true, \"uv\": false}, 8: h'610C58506C864A708E26DD0CCF4BE3D9', 9: 1}",
|
||||
"description": "make credential parameters 3"
|
||||
},
|
||||
{
|
||||
"hex": "a5015820687134968222ec17202e42505f8ed2b16ae22f16bb05b88c25db9e602645f14102a26269646b6578616d706c652e636f6d646e616d656441636d6503a462696458203082019330820138a0030201023082019330820138a0030201023082019330826469636f6e782b68747470733a2f2f706963732e6578616d706c652e636f6d2f30302f702f61426a6a6a707150622e706e67646e616d65766a6f686e70736d697468406578616d706c652e636f6d6b646973706c61794e616d656d4a6f686e20502e20536d6974680482a263616c672664747970656a7075626C69632D6B6579a263616c6739010064747970656a7075626C69632D6B657907a162726bf5",
|
||||
"cbor": "{1: h'687134968222EC17202E42505F8ED2B16AE22F16BB05B88C25DB9E602645F141', 2: {\"id\": \"example.com\", \"name\": \"Acme\"}, 3: {\"id\": h'3082019330820138A0030201023082019330820138A003020102308201933082', \"icon\": \"https://pics.example.com/00/p/aBjjjpqPb.png\", \"name\": \"johnpsmith@example.com\", \"displayName\": \"John P. Smith\"}, 4: [{\"alg\": -7, \"type\": \"public-key\"}, {\"alg\": -257, \"type\": \"public-key\"}], 7: {\"rk\": true}}",
|
||||
"description": "make credential parameters 4 (ex 4)"
|
||||
},
|
||||
{
|
||||
"hex": "a301667061636b65640258f49d04bd8a43be42e45b38aee975ed4ff0b525e745051ac08014260bc12c86e7dd4100000000e00f21f9fc624cf200000000000000000070a148e03e8a315920691cead2a56117675a35857539dc16b51cc12a3acb525baeb124377f38026c29bf42f5b840285c1cf4b81783f3279f224b52dbff40523df87cc2a391a3ab888356002a5c7478385c9cc74fd0aea2a721247fb9023b3e4f6b080c59ebf62f2faa8345693dcb481932a50102032620012158202e3deacb152877fccbdc3bb03694178ba1c48fdd3943d49701c30a65144405202258204cdb1d0b76685e2652dfec4b5558e7e6dbf093dac0139919f9e14de98d0825cd03a263616c67266373696758453043022034870247c8292052f01ed3be4eccd22bb0ebb0344affce83733e2ac978f0d48b021f7f955405cb09b60f005c1c243f492865e2ca70871aeb35c7791365430ad4bd",
|
||||
"cbor": "{1: \"packed\", 2: h'9D04BD8A43BE42E45B38AEE975ED4FF0B525E745051AC08014260BC12C86E7DD4100000000E00F21F9FC624CF200000000000000000070A148E03E8A315920691CEAD2A56117675A35857539DC16B51CC12A3ACB525BAEB124377F38026C29BF42F5B840285C1CF4B81783F3279F224B52DBFF40523DF87CC2A391A3AB888356002A5C7478385C9CC74FD0AEA2A721247FB9023B3E4F6B080C59EBF62F2FAA8345693DCB481932A50102032620012158202E3DEACB152877FCCBDC3BB03694178BA1C48FDD3943D49701C30A65144405202258204CDB1D0B76685E2652DFEC4B5558E7E6DBF093DAC0139919F9E14DE98D0825CD', 3: {\"alg\": -7, \"sig\": h'3043022034870247C8292052F01ED3BE4ECCD22BB0EBB0344AFFCE83733E2AC978F0D48B021F7F955405CB09B60F005C1C243F492865E2CA70871AEB35C7791365430AD4BD'}}",
|
||||
"description": "get assertion parameters 1"
|
||||
},
|
||||
{
|
||||
"hex": "a70178196765745f6261645f74797065732e6578616d706c652e636f6d025820cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd0381a262696458701c0746a765e28acf5305ed91025263648e4b95fe94b19ecc63fa01e2a5b83a933755ca8d7e4c460c6cca9be1bba428c8b3079920e4f5bf4abd327abeb0bc731fedd34f7fe15ad2e45290945122d38f9b1462618a186abd66209aebb8886fad71ae1e83429b628b096b7f2db0c157c00e64747970656a7075626c69632d6b657904a005a2627570f4627576f40650610c58506c864a708e26dd0ccf4be3d90701",
|
||||
"cbor": "{1: \"get_bad_types.example.com\", 2: h'CDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCD', 3: [{\"id\": h'1C0746A765E28ACF5305ED91025263648E4B95FE94B19ECC63FA01E2A5B83A933755CA8D7E4C460C6CCA9BE1BBA428C8B3079920E4F5BF4ABD327ABEB0BC731FEDD34F7FE15AD2E45290945122D38F9B1462618A186ABD66209AEBB8886FAD71AE1E83429B628B096B7F2DB0C157C00E', \"type\": \"public-key\"}], 4: {}, 5: {\"up\": false, \"uv\": false}, 6: h'610C58506C864A708E26DD0CCF4BE3D9', 7: 1}",
|
||||
"description": "get assertion parameters 2"
|
||||
},
|
||||
{
|
||||
"hex": "a70178196765745f6261645f74797065732e6578616d706c652e636f6d025820cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd0381a262696458701c0746a765e28acf5305ed91025263648e4b95fe94b19ecc63fa01e2a5b83a933755ca8d7e4c460c6cca9be1bba428c8b3079920e4f5bf4abd327abeb0bc731fedd34f7fe15ad2e45290945122d38f9b1462618a186abd66209aebb8886fad71ae1e83429b628b096b7f2db0c157c00e64747970656a7075626c69632d6b657904a00650610c58506c864a708e26dd0ccf4be3d90701",
|
||||
"cbor": "{1: \"get_bad_types.example.com\", 2: h'CDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCD', 3: [{\"id\": h'1C0746A765E28ACF5305ED91025263648E4B95FE94B19ECC63FA01E2A5B83A933755CA8D7E4C460C6CCA9BE1BBA428C8B3079920E4F5BF4ABD327ABEB0BC731FEDD34F7FE15AD2E45290945122D38F9B1462618A186ABD66209AEBB8886FAD71AE1E83429B628B096B7F2DB0C157C00E', \"type\": \"public-key\"}], 4: {},6: h'610C58506C864A708E26DD0CCF4BE3D9', 7: 1}",
|
||||
"description": "get assertion parameters 3 (no key 5)"
|
||||
},
|
||||
{
|
||||
"hex": "a4016b6578616d706c652e636f6d025820687134968222ec17202e42505f8ed2b1687134968222ec17202e42505f8ed2b10382a26269645840f22006de4f905af68a43942f024f2a5ece603d9c6d4b3df8be08ed01fc442646d034858ac75bed3fd580bf9808d94fcbee82b9b2ef6677af0adcc35852ea6b9e64747970656a7075626C69632D6B6579a26269645832030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030364747970656a7075626C69632D6B657905a1627576f5",
|
||||
"cbor": "{1: \"example.com\", 2: h'687134968222EC17202E42505F8ED2B1687134968222EC17202E42505F8ED2B1', 3: [{\"id\": h'F22006DE4F905AF68A43942F024F2A5ECE603D9C6D4B3DF8BE08ED01FC442646D034858AC75BED3FD580BF9808D94FCBEE82B9B2EF6677AF0ADCC35852EA6B9E', \"type\": \"public-key\"}, {\"id\": h'0303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303', \"type\": \"public-key\"}], 5: {\"uv\": true}}",
|
||||
"description": "get assertion parameters 4 (ex 5)"
|
||||
},
|
||||
{
|
||||
"hex": "a201010202",
|
||||
"cbor": "{1: 1, 2: 2}",
|
||||
"description": "client pin parameters 1 (only key 1,2)"
|
||||
},
|
||||
{
|
||||
"hex": "a50101020303a501020338182001215820b20717fbc7c82517f511027d9e80888abd33a1837ce835a50ceffd4dea14337b2258209d132823edd852dcc21e4923168df96fe69ea591e1c2d13e98e4920673ec31b004400540",
|
||||
"cbor": "{1: 1, 2: 3, 3: {1: 2, 3: -25, -1: 1, -2: h'B20717FBC7C82517F511027D9E80888ABD33A1837CE835A50CEFFD4DEA14337B', -3: h'9D132823EDD852DCC21E4923168DF96FE69EA591E1C2D13E98E4920673EC31B0'}, 4: h'', 5: h''}",
|
||||
"description": "client pin parameters 2"
|
||||
},
|
||||
{
|
||||
"hex": "a50101020303a50102033818200121582060a086e3e9d1d95618826e706000a66b0809fadd29fbc50bb430d1fd21512f89225820c7d8411433be1e2728a397c66cca8d8b36b738cda54ee027d5efaf72c0db050a04504518a1ba83801245c6f8cad90952cda5055840a9eed54033b9f8fad7f76c69c8469f69c2e623ccb7819a31520b4da7756fc9bd1d4d4fc8d82df3284e9b3f600f03e994c6492a75fc2ed660a33ad343917aa7e2",
|
||||
"cbor": "{1: 1, 2: 3, 3: {1: 2, 3: -25, -1: 1, -2: h'60A086E3E9D1D95618826E706000A66B0809FADD29FBC50BB430D1FD21512F89', -3: h'C7D8411433BE1E2728A397C66CCA8D8B36B738CDA54EE027D5EFAF72C0DB050A'}, 4: h'4518A1BA83801245C6F8CAD90952CDA5', 5: h'A9EED54033B9F8FAD7F76C69C8469F69C2E623CCB7819A31520B4DA7756FC9BD1D4D4FC8D82DF3284E9B3F600F03E994C6492A75FC2ED660A33AD343917AA7E2'}",
|
||||
"description": "client pin parameters 3"
|
||||
},
|
||||
{
|
||||
"hex": "a40101020503a50102033818200121582060a086e3e9d1d95618826e706000a66b0809fadd29fbc50bb430d1fd21512f89225820c7d8411433be1e2728a397c66cca8d8b36b738cda54ee027d5efaf72c0db050a06509cac212d435c7f03d0ffa29caedf0e35",
|
||||
"cbor": "{1: 1, 2: 5, 3: {1: 2, 3: -25, -1: 1, -2: h'60A086E3E9D1D95618826E706000A66B0809FADD29FBC50BB430D1FD21512F89', -3: h'C7D8411433BE1E2728A397C66CCA8D8B36B738CDA54EE027D5EFAF72C0DB050A'}, 6: h'9CAC212D435C7F03D0FFA29CAEDF0E35'}",
|
||||
"description": "client pin parameters 4"
|
||||
}
|
||||
]
|
||||
14
libraries/opensk/fuzz/fuzz_helper/Cargo.toml
Normal file
14
libraries/opensk/fuzz/fuzz_helper/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "fuzz_helper"
|
||||
version = "0.1.0"
|
||||
authors = ["Mingxiao Guo <mingxguo@google.com>"]
|
||||
license = "Apache-2.0"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
arrayref = "0.3.6"
|
||||
opensk = { path = "../..", features = ["fuzz"] }
|
||||
crypto = { path = "../../../crypto", features = ['std'] }
|
||||
rng256 = { path = "../../../rng256", features = ['std'] }
|
||||
sk-cbor = { path = "../../../cbor" }
|
||||
arbitrary = { version = "0.4.7", features = ["derive"] }
|
||||
276
libraries/opensk/fuzz/fuzz_helper/src/lib.rs
Normal file
276
libraries/opensk/fuzz/fuzz_helper/src/lib.rs
Normal file
@@ -0,0 +1,276 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use arbitrary::{Arbitrary, Unstructured};
|
||||
use arrayref::array_ref;
|
||||
use core::convert::TryFrom;
|
||||
use opensk::api::customization::is_valid;
|
||||
use opensk::ctap::command::{
|
||||
AuthenticatorClientPinParameters, AuthenticatorGetAssertionParameters,
|
||||
AuthenticatorMakeCredentialParameters, Command,
|
||||
};
|
||||
use opensk::ctap::data_formats::EnterpriseAttestationMode;
|
||||
use opensk::ctap::hid::{
|
||||
ChannelID, CtapHidCommand, HidPacket, HidPacketIterator, Message, MessageAssembler,
|
||||
};
|
||||
use opensk::ctap::{cbor_read, Channel, CtapState};
|
||||
use opensk::env::test::customization::TestCustomization;
|
||||
use opensk::env::test::TestEnv;
|
||||
use opensk::{test_helpers, Ctap, Transport};
|
||||
|
||||
const CHANNEL_BROADCAST: ChannelID = [0xFF, 0xFF, 0xFF, 0xFF];
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum InputType {
|
||||
CborMakeCredentialParameter,
|
||||
CborGetAssertionParameter,
|
||||
CborClientPinParameter,
|
||||
Ctap1,
|
||||
}
|
||||
|
||||
pub enum FuzzError {
|
||||
ArbitraryError(arbitrary::Error),
|
||||
InvalidCustomization,
|
||||
}
|
||||
|
||||
pub type FuzzResult<T> = Result<T, FuzzError>;
|
||||
|
||||
impl From<arbitrary::Error> for FuzzError {
|
||||
fn from(err: arbitrary::Error) -> Self {
|
||||
Self::ArbitraryError(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Converts a byte slice into Message
|
||||
fn raw_to_message(data: &[u8]) -> Message {
|
||||
if data.len() <= 4 {
|
||||
let mut cid = [0; 4];
|
||||
cid[..data.len()].copy_from_slice(data);
|
||||
Message {
|
||||
cid,
|
||||
// Arbitrary command.
|
||||
cmd: CtapHidCommand::Cbor,
|
||||
payload: vec![],
|
||||
}
|
||||
} else {
|
||||
Message {
|
||||
cid: array_ref!(data, 0, 4).clone(),
|
||||
cmd: CtapHidCommand::from(data[4]),
|
||||
payload: data[5..].to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns an initialized ctap state, hid and the allocated cid
|
||||
// after processing the init command.
|
||||
fn initialize(ctap: &mut Ctap<TestEnv>) -> ChannelID {
|
||||
let nonce = vec![0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0];
|
||||
let message = Message {
|
||||
cid: CHANNEL_BROADCAST,
|
||||
cmd: CtapHidCommand::Init,
|
||||
payload: nonce,
|
||||
};
|
||||
let mut assembler_reply = MessageAssembler::default();
|
||||
let mut result_cid: ChannelID = Default::default();
|
||||
for pkt_request in HidPacketIterator::new(message).unwrap() {
|
||||
for pkt_reply in ctap.process_hid_packet(&pkt_request, Transport::MainHid) {
|
||||
if let Ok(Some(result)) = assembler_reply.parse_packet(ctap.env(), &pkt_reply) {
|
||||
result_cid.copy_from_slice(&result.payload[8..12]);
|
||||
}
|
||||
}
|
||||
}
|
||||
result_cid
|
||||
}
|
||||
|
||||
// Checks whether the given data can be interpreted as the given type.
|
||||
fn is_type(data: &[u8], input_type: InputType) -> bool {
|
||||
if input_type == InputType::Ctap1 {
|
||||
return true;
|
||||
}
|
||||
match cbor_read(data) {
|
||||
Err(_) => false,
|
||||
Ok(decoded_cbor) => match input_type {
|
||||
InputType::CborMakeCredentialParameter => {
|
||||
AuthenticatorMakeCredentialParameters::try_from(decoded_cbor).is_ok()
|
||||
}
|
||||
InputType::CborGetAssertionParameter => {
|
||||
AuthenticatorGetAssertionParameters::try_from(decoded_cbor).is_ok()
|
||||
}
|
||||
InputType::CborClientPinParameter => {
|
||||
AuthenticatorClientPinParameters::try_from(decoded_cbor).is_ok()
|
||||
}
|
||||
_ => true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Interprets the raw data as a complete message (with channel id, command type and payload) and
|
||||
// invokes message splitting, packet processing at CTAP HID level and response assembling.
|
||||
fn process_message(data: &[u8], ctap: &mut Ctap<TestEnv>) {
|
||||
let message = raw_to_message(data);
|
||||
if let Some(hid_packet_iterator) = HidPacketIterator::new(message) {
|
||||
let mut assembler_reply = MessageAssembler::default();
|
||||
for pkt_request in hid_packet_iterator {
|
||||
for pkt_reply in ctap.process_hid_packet(&pkt_request, Transport::MainHid) {
|
||||
// Only checks for assembling crashes, not for semantics.
|
||||
let _ = assembler_reply.parse_packet(ctap.env(), &pkt_reply);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Interprets the raw data as any ctap command (including the command byte) and
|
||||
// invokes message splitting, packet processing at CTAP HID level and response assembling
|
||||
// using an initialized and allocated channel.
|
||||
pub fn process_ctap_any_type(data: &[u8]) -> arbitrary::Result<()> {
|
||||
let mut unstructured = Unstructured::new(data);
|
||||
|
||||
let mut env = TestEnv::default();
|
||||
env.rng().seed_from_u64(u64::arbitrary(&mut unstructured)?);
|
||||
|
||||
let data = unstructured.take_rest();
|
||||
// Initialize ctap state and hid and get the allocated cid.
|
||||
let mut ctap = Ctap::new(env);
|
||||
let cid = initialize(&mut ctap);
|
||||
// Wrap input as message with the allocated cid.
|
||||
let mut command = cid.to_vec();
|
||||
command.extend(data);
|
||||
process_message(&command, &mut ctap);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_customization(
|
||||
unstructured: &mut Unstructured,
|
||||
customization: &mut TestCustomization,
|
||||
) -> FuzzResult<()> {
|
||||
customization.setup_enterprise_attestation(
|
||||
Option::<EnterpriseAttestationMode>::arbitrary(unstructured)?,
|
||||
// TODO: Generate arbitrary rp_id_list (but with some dummies because content doesn't
|
||||
// matter), and use the rp ids in commands.
|
||||
None,
|
||||
);
|
||||
if !is_valid(customization) {
|
||||
return Err(FuzzError::InvalidCustomization);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_state(
|
||||
unstructured: &mut Unstructured,
|
||||
state: &mut CtapState<TestEnv>,
|
||||
env: &mut TestEnv,
|
||||
) -> FuzzResult<()> {
|
||||
if bool::arbitrary(unstructured)? {
|
||||
test_helpers::enable_enterprise_attestation(state, env).ok();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Interprets the raw data as of the given input type and
|
||||
// invokes message splitting, packet processing at CTAP HID level and response assembling
|
||||
// using an initialized and allocated channel.
|
||||
pub fn process_ctap_specific_type(data: &[u8], input_type: InputType) -> arbitrary::Result<()> {
|
||||
let mut unstructured = Unstructured::new(data);
|
||||
|
||||
let mut env = TestEnv::default();
|
||||
env.rng().seed_from_u64(u64::arbitrary(&mut unstructured)?);
|
||||
|
||||
let data = unstructured.take_rest();
|
||||
if !is_type(data, input_type) {
|
||||
return Ok(());
|
||||
}
|
||||
// Initialize ctap state and hid and get the allocated cid.
|
||||
let mut ctap = Ctap::new(env);
|
||||
let cid = initialize(&mut ctap);
|
||||
// Wrap input as message with allocated cid and command type.
|
||||
let mut command = cid.to_vec();
|
||||
match input_type {
|
||||
InputType::CborMakeCredentialParameter => {
|
||||
command.extend(&[0x10, 0x01]);
|
||||
}
|
||||
InputType::CborGetAssertionParameter => {
|
||||
command.extend(&[0x10, 0x02]);
|
||||
}
|
||||
InputType::CborClientPinParameter => {
|
||||
command.extend(&[0x10, 0x06]);
|
||||
}
|
||||
InputType::Ctap1 => {
|
||||
command.extend(&[0x03]);
|
||||
}
|
||||
}
|
||||
command.extend(data);
|
||||
process_message(&command, &mut ctap);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process_ctap_structured(data: &[u8], input_type: InputType) -> FuzzResult<()> {
|
||||
let unstructured = &mut Unstructured::new(data);
|
||||
|
||||
let mut env = TestEnv::default();
|
||||
env.rng().seed_from_u64(u64::arbitrary(unstructured)?);
|
||||
setup_customization(unstructured, env.customization_mut())?;
|
||||
|
||||
let mut state = CtapState::new(&mut env);
|
||||
setup_state(unstructured, &mut state, &mut env)?;
|
||||
|
||||
let command = match input_type {
|
||||
InputType::CborMakeCredentialParameter => Command::AuthenticatorMakeCredential(
|
||||
AuthenticatorMakeCredentialParameters::arbitrary(unstructured)?,
|
||||
),
|
||||
InputType::CborGetAssertionParameter => Command::AuthenticatorGetAssertion(
|
||||
AuthenticatorGetAssertionParameters::arbitrary(unstructured)?,
|
||||
),
|
||||
InputType::CborClientPinParameter => Command::AuthenticatorClientPin(
|
||||
AuthenticatorClientPinParameters::arbitrary(unstructured)?,
|
||||
),
|
||||
InputType::Ctap1 => {
|
||||
unimplemented!()
|
||||
}
|
||||
};
|
||||
|
||||
state
|
||||
.process_parsed_command(
|
||||
&mut env,
|
||||
command,
|
||||
Channel::MainHid(ChannelID::arbitrary(unstructured)?),
|
||||
)
|
||||
.ok();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Splits the given data as HID packets and reassembles it, verifying that the original input message is reconstructed.
|
||||
pub fn split_assemble_hid_packets(data: &[u8]) -> arbitrary::Result<()> {
|
||||
let mut unstructured = Unstructured::new(data);
|
||||
|
||||
let mut env = TestEnv::default();
|
||||
env.rng().seed_from_u64(u64::arbitrary(&mut unstructured)?);
|
||||
|
||||
let data = unstructured.take_rest();
|
||||
let message = raw_to_message(data);
|
||||
if let Some(hid_packet_iterator) = HidPacketIterator::new(message.clone()) {
|
||||
let mut assembler = MessageAssembler::default();
|
||||
let packets: Vec<HidPacket> = hid_packet_iterator.collect();
|
||||
if let Some((last_packet, first_packets)) = packets.split_last() {
|
||||
for packet in first_packets {
|
||||
assert_eq!(assembler.parse_packet(&mut env, packet), Ok(None));
|
||||
}
|
||||
assert_eq!(
|
||||
assembler.parse_packet(&mut env, last_packet),
|
||||
Ok(Some(message))
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#![no_main]
|
||||
|
||||
use fuzz_helper::{process_ctap_specific_type, InputType};
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
// Fuzz inputs as CTAP1 U2F raw messages.
|
||||
// For a more generic fuzz target including all CTAP commands, you can use
|
||||
// fuzz_target_process_ctap_command.
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
process_ctap_specific_type(data, InputType::Ctap1).ok();
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
#![no_main]
|
||||
|
||||
use fuzz_helper::{process_ctap_specific_type, InputType};
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
// Fuzz inputs as CTAP2 client pin command parameters encoded in cbor.
|
||||
// For a more generic fuzz target including all CTAP commands, you can use
|
||||
// fuzz_target_process_ctap_command.
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
process_ctap_specific_type(data, InputType::CborClientPinParameter).ok();
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
#![no_main]
|
||||
|
||||
use fuzz_helper::{process_ctap_structured, InputType};
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
// Fuzz inputs as CTAP2 client pin command parameters.
|
||||
// The inputs will used to construct arbitrary client pin parameters.
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
process_ctap_structured(data, InputType::CborClientPinParameter).ok();
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
#![no_main]
|
||||
|
||||
use fuzz_helper::{process_ctap_specific_type, InputType};
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
// Fuzz inputs as CTAP2 get assertion command parameters encoded in cbor.
|
||||
// For a more generic fuzz target including all CTAP commands, you can use
|
||||
// fuzz_target_process_ctap_command.
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
process_ctap_specific_type(data, InputType::CborGetAssertionParameter).ok();
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
#![no_main]
|
||||
|
||||
use fuzz_helper::{process_ctap_structured, InputType};
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
// Fuzz inputs as CTAP2 get assertion command parameters.
|
||||
// The inputs will used to construct arbitrary get assertion parameters.
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
process_ctap_structured(data, InputType::CborGetAssertionParameter).ok();
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
#![no_main]
|
||||
|
||||
use fuzz_helper::{process_ctap_specific_type, InputType};
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
// Fuzz inputs as CTAP2 make credential command parameters encoded in cbor.
|
||||
// For a more generic fuzz target including all CTAP commands, you can use
|
||||
// fuzz_target_process_ctap_command.
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
process_ctap_specific_type(data, InputType::CborMakeCredentialParameter).ok();
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
#![no_main]
|
||||
|
||||
use fuzz_helper::{process_ctap_structured, InputType};
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
// Fuzz inputs as CTAP2 make credential command parameters.
|
||||
// The inputs will used to construct arbitrary make credential parameters.
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
process_ctap_structured(data, InputType::CborMakeCredentialParameter).ok();
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
#![no_main]
|
||||
|
||||
use fuzz_helper::process_ctap_any_type;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
// Generically fuzz inputs as CTAP commands.
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
process_ctap_any_type(data).ok();
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
#![no_main]
|
||||
|
||||
use fuzz_helper::split_assemble_hid_packets;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
// Fuzzing HID packets splitting and assembling functions.
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
split_assemble_hid_packets(data).ok();
|
||||
});
|
||||
60
libraries/opensk/fuzz/make_corpus.py
Normal file
60
libraries/opensk/fuzz/make_corpus.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""Creates a directory containing seed inputs from a json file having
|
||||
the following structure:
|
||||
[
|
||||
{
|
||||
"hex": "a901a1182a182a02a3626964781a6d616b655f6261645f7...",
|
||||
"cbor": "{1: h'42', 2: {\"id\": \"make.example.com\", ...",
|
||||
"description": "make credential parameters"
|
||||
},
|
||||
...
|
||||
]
|
||||
|
||||
Usage:
|
||||
- pass the resulting corpus directory path as the first argument
|
||||
- pass the json file path to make the corpus from as the second argument
|
||||
Example:
|
||||
python make_corpus.py ./corpus ./corpus_file.json
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os.path
|
||||
|
||||
|
||||
# Creates a corpus directory to the given path from the given json file.
|
||||
def make_corpus(corpus_dir, corpus_json):
|
||||
if not os.path.exists(corpus_dir):
|
||||
os.makedirs(corpus_dir)
|
||||
elif not os.path.isdir(corpus_dir):
|
||||
raise NotADirectoryError
|
||||
|
||||
if os.path.isfile(corpus_json) and \
|
||||
os.path.splitext(corpus_json)[-1] == ".json":
|
||||
with open(corpus_json, encoding="utf-8") as corpus_file:
|
||||
corpus = json.load(corpus_file)
|
||||
else:
|
||||
raise TypeError
|
||||
|
||||
for i, seed_file in enumerate(corpus):
|
||||
seed_file_name = "seed_file_" + str(i)
|
||||
raw_hex = seed_file["hex"].decode("hex")
|
||||
with open(os.path.join(corpus_dir, seed_file_name), "wb") as f:
|
||||
f.write(raw_hex)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"corpus_directory", help="the resulting corpus directory path")
|
||||
parser.add_argument(
|
||||
"corpus_json", help="the json file path to make the corpus from")
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
make_corpus(args.corpus_directory, args.corpus_json)
|
||||
except NotADirectoryError:
|
||||
print(args.corpus_directory, " is not a directory.\n")
|
||||
except TypeError:
|
||||
print(args.corpus_json, " must be a json file.\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
114
libraries/opensk/src/api/attestation_store.rs
Normal file
114
libraries/opensk/src/api/attestation_store.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
// Copyright 2022-2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use persistent_store::{StoreError, StoreUpdate};
|
||||
|
||||
use crate::env::Env;
|
||||
|
||||
/// Identifies an attestation.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub enum Id {
|
||||
Batch,
|
||||
Enterprise,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "std", derive(Debug, PartialEq, Eq))]
|
||||
pub struct Attestation {
|
||||
/// ECDSA private key (big-endian).
|
||||
pub private_key: [u8; 32],
|
||||
pub certificate: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Stores enterprise or batch attestations.
|
||||
pub trait AttestationStore {
|
||||
/// Returns an attestation given its id, if it exists.
|
||||
///
|
||||
/// This should always return the attestation. Checking whether it is ok to use the attestation
|
||||
/// is done in the CTAP library.
|
||||
fn get(&mut self, id: &Id) -> Result<Option<Attestation>, Error>;
|
||||
|
||||
/// Sets the attestation for a given id.
|
||||
///
|
||||
/// This function may not be supported.
|
||||
fn set(&mut self, id: &Id, attestation: Option<&Attestation>) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
/// Attestation store errors.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
Storage,
|
||||
Internal,
|
||||
NoSupport,
|
||||
}
|
||||
|
||||
/// Keys of the environment store reserved for the attestation store.
|
||||
pub const STORAGE_KEYS: &[usize] = &[1, 2];
|
||||
|
||||
pub fn helper_get(env: &mut impl Env) -> Result<Option<Attestation>, Error> {
|
||||
let private_key = env.store().find(PRIVATE_KEY_STORAGE_KEY)?;
|
||||
let certificate = env.store().find(CERTIFICATE_STORAGE_KEY)?;
|
||||
let (private_key, certificate) = match (private_key, certificate) {
|
||||
(Some(x), Some(y)) => (x, y),
|
||||
(None, None) => return Ok(None),
|
||||
_ => return Err(Error::Internal),
|
||||
};
|
||||
if private_key.len() != 32 {
|
||||
return Err(Error::Internal);
|
||||
}
|
||||
Ok(Some(Attestation {
|
||||
private_key: *array_ref![private_key, 0, 32],
|
||||
certificate,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn helper_set(env: &mut impl Env, attestation: Option<&Attestation>) -> Result<(), Error> {
|
||||
let updates = match attestation {
|
||||
None => [
|
||||
StoreUpdate::Remove {
|
||||
key: PRIVATE_KEY_STORAGE_KEY,
|
||||
},
|
||||
StoreUpdate::Remove {
|
||||
key: CERTIFICATE_STORAGE_KEY,
|
||||
},
|
||||
],
|
||||
Some(attestation) => [
|
||||
StoreUpdate::Insert {
|
||||
key: PRIVATE_KEY_STORAGE_KEY,
|
||||
value: &attestation.private_key[..],
|
||||
},
|
||||
StoreUpdate::Insert {
|
||||
key: CERTIFICATE_STORAGE_KEY,
|
||||
value: &attestation.certificate[..],
|
||||
},
|
||||
],
|
||||
};
|
||||
Ok(env.store().transaction(&updates)?)
|
||||
}
|
||||
|
||||
const PRIVATE_KEY_STORAGE_KEY: usize = STORAGE_KEYS[0];
|
||||
const CERTIFICATE_STORAGE_KEY: usize = STORAGE_KEYS[1];
|
||||
|
||||
impl From<StoreError> for Error {
|
||||
fn from(error: StoreError) -> Self {
|
||||
match error {
|
||||
StoreError::InvalidArgument
|
||||
| StoreError::NoCapacity
|
||||
| StoreError::NoLifetime
|
||||
| StoreError::InvalidStorage => Error::Internal,
|
||||
StoreError::StorageError => Error::Storage,
|
||||
}
|
||||
}
|
||||
}
|
||||
39
libraries/opensk/src/api/clock.rs
Normal file
39
libraries/opensk/src/api/clock.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright 2022-2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
pub trait Clock {
|
||||
/// Stores data for the clock to recognize if this timer is elapsed or not.
|
||||
///
|
||||
/// The Clock does not keep track of the timers it creates. Therefore, they should not wrap
|
||||
/// unexpectedly. A timer that is elapsed may never return to a non-elapsed state.
|
||||
///
|
||||
/// A default Timer should return `true` when checked with `is_elapsed`.
|
||||
type Timer: Default;
|
||||
|
||||
/// Creates a new timer that expires after the given time in ms.
|
||||
fn make_timer(&mut self, milliseconds: usize) -> Self::Timer;
|
||||
|
||||
/// Checks whether a given timer is expired.
|
||||
///
|
||||
/// Until a timer expires, this function consistently returns false. Once it expires, this
|
||||
/// function consistently returns true. In particular, it is valid to continue calling this
|
||||
/// function after the first time it returns true.
|
||||
fn is_elapsed(&mut self, timer: &Self::Timer) -> bool;
|
||||
|
||||
/// Timestamp in microseconds.
|
||||
///
|
||||
/// Normal operation only needs relative time, absolute timestamps are useful for debugging.
|
||||
#[cfg(feature = "debug_ctap")]
|
||||
fn timestamp_us(&mut self) -> usize;
|
||||
}
|
||||
49
libraries/opensk/src/api/connection.rs
Normal file
49
libraries/opensk/src/api/connection.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright 2022-2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use core::convert::TryFrom;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum UsbEndpoint {
|
||||
MainHid = 1,
|
||||
#[cfg(feature = "vendor_hid")]
|
||||
VendorHid = 2,
|
||||
}
|
||||
|
||||
impl TryFrom<usize> for UsbEndpoint {
|
||||
type Error = SendOrRecvError;
|
||||
|
||||
fn try_from(endpoint_num: usize) -> Result<Self, SendOrRecvError> {
|
||||
match endpoint_num {
|
||||
1 => Ok(UsbEndpoint::MainHid),
|
||||
#[cfg(feature = "vendor_hid")]
|
||||
2 => Ok(UsbEndpoint::VendorHid),
|
||||
_ => Err(SendOrRecvError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum SendOrRecvStatus {
|
||||
Timeout,
|
||||
Sent,
|
||||
Received(UsbEndpoint),
|
||||
}
|
||||
|
||||
pub struct SendOrRecvError;
|
||||
|
||||
pub type SendOrRecvResult = Result<SendOrRecvStatus, SendOrRecvError>;
|
||||
|
||||
pub trait HidConnection {
|
||||
fn send_and_maybe_recv(&mut self, buf: &mut [u8; 64], timeout_ms: usize) -> SendOrRecvResult;
|
||||
}
|
||||
460
libraries/opensk/src/api/customization.rs
Normal file
460
libraries/opensk/src/api/customization.rs
Normal file
@@ -0,0 +1,460 @@
|
||||
// Copyright 2022-2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! This file contains all customizable constants.
|
||||
//!
|
||||
//! If you adapt them, make sure to run the tests before flashing the firmware.
|
||||
//! Our deploy script enforces the invariants.
|
||||
|
||||
use crate::ctap::data_formats::{CredentialProtectionPolicy, EnterpriseAttestationMode};
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
pub const AAGUID_LENGTH: usize = 16;
|
||||
|
||||
pub trait Customization {
|
||||
/// Authenticator Attestation Global Unique Identifier
|
||||
fn aaguid(&self) -> &'static [u8; AAGUID_LENGTH];
|
||||
|
||||
// ###########################################################################
|
||||
// Constants for adjusting privacy and protection levels.
|
||||
// ###########################################################################
|
||||
|
||||
/// Removes support for PIN protocol v1.
|
||||
///
|
||||
/// We support PIN protocol v2, "intended to aid FIPS certification".
|
||||
/// To certify, you might want to remove support for v1 using this customization.
|
||||
fn allows_pin_protocol_v1(&self) -> bool;
|
||||
|
||||
/// Changes the default level for the credProtect extension.
|
||||
///
|
||||
/// You can change this value to one of the following for more privacy:
|
||||
/// - CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList
|
||||
/// - CredentialProtectionPolicy::UserVerificationRequired
|
||||
///
|
||||
/// UserVerificationOptionalWithCredentialIdList
|
||||
/// Resident credentials are discoverable with
|
||||
/// - an allowList,
|
||||
/// - an excludeList,
|
||||
/// - user verification.
|
||||
///
|
||||
/// UserVerificationRequired
|
||||
/// Resident credentials are discoverable with user verification only.
|
||||
///
|
||||
/// This can improve privacy, but can make usage less comfortable.
|
||||
fn default_cred_protect(&self) -> Option<CredentialProtectionPolicy>;
|
||||
|
||||
/// Sets the initial minimum PIN length in code points.
|
||||
///
|
||||
/// # Invariant
|
||||
///
|
||||
/// - The minimum PIN length must be at least 4.
|
||||
/// - The minimum PIN length must be at most 63.
|
||||
/// - default_min_pin_length_rp_ids() must be non-empty if max_rp_ids_length() is 0.
|
||||
///
|
||||
/// Requiring longer PINs can help establish trust between users and relying
|
||||
/// parties. It makes user verification harder to break, but less convenient.
|
||||
/// NIST recommends at least 6-digit PINs in section 5.1.9.1:
|
||||
/// https://pages.nist.gov/800-63-3/sp800-63b.html
|
||||
///
|
||||
/// Reset reverts the minimum PIN length to this DEFAULT_MIN_PIN_LENGTH.
|
||||
fn default_min_pin_length(&self) -> u8;
|
||||
|
||||
/// Lists relying parties that can read the minimum PIN length.
|
||||
///
|
||||
/// # Invariant
|
||||
///
|
||||
/// - default_min_pin_length_rp_ids() must be non-empty if max_rp_ids_length() is 0
|
||||
///
|
||||
/// Only the RP IDs listed in default_min_pin_length_rp_ids are allowed to read
|
||||
/// the minimum PIN length with the minPinLength extension.
|
||||
fn default_min_pin_length_rp_ids(&self) -> Vec<String>;
|
||||
|
||||
/// Enforces the alwaysUv option.
|
||||
///
|
||||
/// When setting to true, commands require a PIN.
|
||||
/// Also, alwaysUv can not be disabled by commands.
|
||||
///
|
||||
/// A certification (additional to FIDO Alliance's) might require enforcing
|
||||
/// alwaysUv. Otherwise, users should have the choice to configure alwaysUv.
|
||||
/// Calling toggleAlwaysUv is preferred over enforcing alwaysUv here.
|
||||
fn enforce_always_uv(&self) -> bool;
|
||||
|
||||
/// Allows usage of enterprise attestation.
|
||||
///
|
||||
/// # Invariant
|
||||
///
|
||||
/// - Enterprise and batch attestation can not both be active.
|
||||
/// - If the mode is VendorFacilitated, enterprise_attestation_mode() must be non-empty.
|
||||
///
|
||||
/// For privacy reasons, it is disabled by default. You can choose between:
|
||||
/// - EnterpriseAttestationMode::VendorFacilitated
|
||||
/// - EnterpriseAttestationMode::PlatformManaged
|
||||
///
|
||||
/// VendorFacilitated
|
||||
/// Enterprise attestation is restricted to enterprise_attestation_mode(). Add your
|
||||
/// enterprises domain, e.g. "example.com", to the list below.
|
||||
///
|
||||
/// PlatformManaged
|
||||
/// All relying parties can request an enterprise attestation. The authenticator
|
||||
/// trusts the platform to filter requests.
|
||||
///
|
||||
/// To enable the feature, send the subcommand enableEnterpriseAttestation in
|
||||
/// AuthenticatorConfig. An enterprise might want to customize the type of
|
||||
/// attestation that is used. OpenSK defaults to batch attestation. Configuring
|
||||
/// individual certificates then makes authenticators identifiable.
|
||||
///
|
||||
/// OpenSK prevents activating batch and enterprise attestation together. The
|
||||
/// current implementation uses the same key material at the moment, and these
|
||||
/// two modes have conflicting privacy guarantees.
|
||||
/// If you implement your own enterprise attestation mechanism, and you want
|
||||
/// batch attestation at the same time, proceed carefully and remove the
|
||||
/// assertion.
|
||||
fn enterprise_attestation_mode(&self) -> Option<EnterpriseAttestationMode>;
|
||||
|
||||
/// Lists relying party IDs that can perform enterprise attestation.
|
||||
///
|
||||
/// # Invariant
|
||||
///
|
||||
/// - If the mode is VendorFacilitated, enterprise_attestation_mode() must be non-empty.
|
||||
///
|
||||
/// This list is only considered if enterprise attestation is used.
|
||||
#[cfg(feature = "std")]
|
||||
fn enterprise_rp_id_list(&self) -> Vec<String>;
|
||||
|
||||
/// Returns whether the rp_id is contained in enterprise_rp_id_list().
|
||||
fn is_enterprise_rp_id(&self, rp_id: &str) -> bool;
|
||||
|
||||
/// Maximum message size send for CTAP commands.
|
||||
///
|
||||
/// The maximum value is 7609, as HID packets can not encode longer messages.
|
||||
/// 1024 is the default mentioned in the authenticatorLargeBlobs commands.
|
||||
/// Larger values are preferred, as that allows more parameters in commands.
|
||||
/// If long commands are too unreliable on your hardware, consider decreasing
|
||||
/// this value.
|
||||
fn max_msg_size(&self) -> usize;
|
||||
|
||||
/// Sets the number of consecutive failed PINs before blocking interaction.
|
||||
///
|
||||
/// # Invariant
|
||||
///
|
||||
/// - CTAP2.0: Maximum PIN retries must be 8.
|
||||
/// - CTAP2.1: Maximum PIN retries must be 8 at most.
|
||||
///
|
||||
/// The fail retry counter is reset after entering the correct PIN.
|
||||
fn max_pin_retries(&self) -> u8;
|
||||
|
||||
/// Enables or disables basic attestation for FIDO2.
|
||||
///
|
||||
/// # Invariant
|
||||
///
|
||||
/// - Enterprise and batch attestation can not both be active (see above).
|
||||
///
|
||||
/// The basic attestation uses the signing key configured with a vendor command
|
||||
/// as a batch key. If you turn batch attestation on, be aware that it is your
|
||||
/// responsibility to safely generate and store the key material. Also, the
|
||||
/// batches must have size of at least 100k authenticators before using new key
|
||||
/// material.
|
||||
/// U2F is unaffected by this setting.
|
||||
///
|
||||
/// https://www.w3.org/TR/webauthn/#attestation
|
||||
fn use_batch_attestation(&self) -> bool;
|
||||
|
||||
/// Enables or disables signature counters.
|
||||
///
|
||||
/// The signature counter is currently implemented as a global counter.
|
||||
/// The specification strongly suggests to have per-credential counters.
|
||||
/// Implementing those means you can't have an infinite amount of server-side
|
||||
/// credentials anymore. Also, since counters need frequent writes on the
|
||||
/// persistent storage, we might need a flash friendly implementation. This
|
||||
/// solution is a compromise to be compatible with U2F and not wasting storage.
|
||||
///
|
||||
/// https://www.w3.org/TR/webauthn/#signature-counter
|
||||
fn use_signature_counter(&self) -> bool;
|
||||
|
||||
// ###########################################################################
|
||||
// Constants for performance optimization or adapting to different hardware.
|
||||
//
|
||||
// Those constants may be modified before compilation to tune the behavior of
|
||||
// the key.
|
||||
// ###########################################################################
|
||||
|
||||
/// Sets the maximum blob size stored with the credBlob extension.
|
||||
///
|
||||
/// # Invariant
|
||||
///
|
||||
/// - The length must be at least 32.
|
||||
/// - OpenSK puts a limit that the length must be at most 64, as it needs to
|
||||
/// be persisted in the credential ID.
|
||||
fn max_cred_blob_length(&self) -> usize;
|
||||
|
||||
/// Limits the number of considered entries in credential lists.
|
||||
///
|
||||
/// # Invariant
|
||||
///
|
||||
/// - This value, if present, must be at least 1 (more is preferred).
|
||||
///
|
||||
/// Depending on your memory, you can use Some(n) to limit request sizes in
|
||||
/// MakeCredential and GetAssertion. This affects allowList and excludeList.
|
||||
fn max_credential_count_in_list(&self) -> Option<usize>;
|
||||
|
||||
/// Limits the size of largeBlobs the authenticator stores.
|
||||
///
|
||||
/// # Invariant
|
||||
///
|
||||
/// - The allowed size must be at least 1024.
|
||||
/// - The array must fit into the shards reserved in storage/key.rs.
|
||||
fn max_large_blob_array_size(&self) -> usize;
|
||||
|
||||
/// Limits the number of RP IDs that can change the minimum PIN length.
|
||||
///
|
||||
/// # Invariant
|
||||
///
|
||||
/// - If this value is 0, default_min_pin_length_rp_ids() must be non-empty.
|
||||
///
|
||||
/// You can use this constant to have an upper limit in storage requirements.
|
||||
/// This might be useful if you want to more reliably predict the remaining
|
||||
/// storage. Stored string can still be of arbitrary length though, until RP ID
|
||||
/// truncation is implemented.
|
||||
/// Outside of memory considerations, you can set this value to 0 if only RP IDs
|
||||
/// in default_min_pin_length_rp_ids() should be allowed to change the minimum
|
||||
/// PIN length.
|
||||
fn max_rp_ids_length(&self) -> usize;
|
||||
|
||||
/// Sets the number of resident keys you can store.
|
||||
///
|
||||
/// # Invariant
|
||||
///
|
||||
/// - The storage key CREDENTIALS must fit at least this number of credentials.
|
||||
///
|
||||
/// Limiting the number of resident keys permits to ensure a minimum number of
|
||||
/// counter increments.
|
||||
/// Let:
|
||||
/// - P the number of pages (NUM_PAGES in the board definition)
|
||||
/// - K the maximum number of resident keys (max_supported_resident_keys())
|
||||
/// - S the maximum size of a resident key (about 500)
|
||||
/// - C the number of erase cycles (10000)
|
||||
/// - I the minimum number of counter increments
|
||||
///
|
||||
/// We have: I = (P * 4084 - 5107 - K * S) / 8 * C
|
||||
///
|
||||
/// With P=20 and K=150, we have I=2M which is enough for 500 increments per day
|
||||
/// for 10 years.
|
||||
fn max_supported_resident_keys(&self) -> usize;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CustomizationImpl {
|
||||
pub aaguid: &'static [u8; AAGUID_LENGTH],
|
||||
pub allows_pin_protocol_v1: bool,
|
||||
pub default_cred_protect: Option<CredentialProtectionPolicy>,
|
||||
pub default_min_pin_length: u8,
|
||||
pub default_min_pin_length_rp_ids: &'static [&'static str],
|
||||
pub enforce_always_uv: bool,
|
||||
pub enterprise_attestation_mode: Option<EnterpriseAttestationMode>,
|
||||
pub enterprise_rp_id_list: &'static [&'static str],
|
||||
pub max_msg_size: usize,
|
||||
pub max_pin_retries: u8,
|
||||
pub use_batch_attestation: bool,
|
||||
pub use_signature_counter: bool,
|
||||
pub max_cred_blob_length: usize,
|
||||
pub max_credential_count_in_list: Option<usize>,
|
||||
pub max_large_blob_array_size: usize,
|
||||
pub max_rp_ids_length: usize,
|
||||
pub max_supported_resident_keys: usize,
|
||||
}
|
||||
|
||||
pub const DEFAULT_CUSTOMIZATION: CustomizationImpl = CustomizationImpl {
|
||||
aaguid: &[0; AAGUID_LENGTH],
|
||||
allows_pin_protocol_v1: true,
|
||||
default_cred_protect: None,
|
||||
default_min_pin_length: 4,
|
||||
default_min_pin_length_rp_ids: &[],
|
||||
enforce_always_uv: false,
|
||||
enterprise_attestation_mode: None,
|
||||
enterprise_rp_id_list: &[],
|
||||
max_msg_size: 7609,
|
||||
max_pin_retries: 8,
|
||||
use_batch_attestation: false,
|
||||
use_signature_counter: true,
|
||||
max_cred_blob_length: 32,
|
||||
max_credential_count_in_list: None,
|
||||
max_large_blob_array_size: 2048,
|
||||
max_rp_ids_length: 8,
|
||||
max_supported_resident_keys: 150,
|
||||
};
|
||||
|
||||
impl Customization for CustomizationImpl {
|
||||
fn aaguid(&self) -> &'static [u8; AAGUID_LENGTH] {
|
||||
self.aaguid
|
||||
}
|
||||
|
||||
fn allows_pin_protocol_v1(&self) -> bool {
|
||||
self.allows_pin_protocol_v1
|
||||
}
|
||||
|
||||
fn default_cred_protect(&self) -> Option<CredentialProtectionPolicy> {
|
||||
self.default_cred_protect
|
||||
}
|
||||
|
||||
fn default_min_pin_length(&self) -> u8 {
|
||||
self.default_min_pin_length
|
||||
}
|
||||
|
||||
fn default_min_pin_length_rp_ids(&self) -> Vec<String> {
|
||||
self.default_min_pin_length_rp_ids
|
||||
.iter()
|
||||
.map(|s| String::from(*s))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn enforce_always_uv(&self) -> bool {
|
||||
self.enforce_always_uv
|
||||
}
|
||||
|
||||
fn enterprise_attestation_mode(&self) -> Option<EnterpriseAttestationMode> {
|
||||
self.enterprise_attestation_mode
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
fn enterprise_rp_id_list(&self) -> Vec<String> {
|
||||
self.enterprise_rp_id_list
|
||||
.iter()
|
||||
.map(|s| String::from(*s))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn is_enterprise_rp_id(&self, rp_id: &str) -> bool {
|
||||
self.enterprise_rp_id_list.contains(&rp_id)
|
||||
}
|
||||
|
||||
fn max_msg_size(&self) -> usize {
|
||||
self.max_msg_size
|
||||
}
|
||||
|
||||
fn max_pin_retries(&self) -> u8 {
|
||||
self.max_pin_retries
|
||||
}
|
||||
|
||||
fn use_batch_attestation(&self) -> bool {
|
||||
self.use_batch_attestation
|
||||
}
|
||||
|
||||
fn use_signature_counter(&self) -> bool {
|
||||
self.use_signature_counter
|
||||
}
|
||||
|
||||
fn max_cred_blob_length(&self) -> usize {
|
||||
self.max_cred_blob_length
|
||||
}
|
||||
|
||||
fn max_credential_count_in_list(&self) -> Option<usize> {
|
||||
self.max_credential_count_in_list
|
||||
}
|
||||
|
||||
fn max_large_blob_array_size(&self) -> usize {
|
||||
self.max_large_blob_array_size
|
||||
}
|
||||
|
||||
fn max_rp_ids_length(&self) -> usize {
|
||||
self.max_rp_ids_length
|
||||
}
|
||||
|
||||
fn max_supported_resident_keys(&self) -> usize {
|
||||
self.max_supported_resident_keys
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
pub fn is_valid(customization: &impl Customization) -> bool {
|
||||
// Two invariants are currently tested in different files:
|
||||
// - storage.rs: if max_large_blob_array_size() fits the shards
|
||||
// - storage/key.rs: if max_supported_resident_keys() fits CREDENTIALS
|
||||
|
||||
// Max message size must be between 1024 and 7609.
|
||||
if customization.max_msg_size() < 1024 || customization.max_msg_size() > 7609 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Default min pin length must be between 4 and 63.
|
||||
if customization.default_min_pin_length() < 4 || customization.default_min_pin_length() > 63 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// OpenSK prevents activating batch and enterprise attestation together. The
|
||||
// current implementation uses the same key material at the moment, and these
|
||||
// two modes have conflicting privacy guarantees.
|
||||
if customization.use_batch_attestation()
|
||||
&& customization.enterprise_attestation_mode().is_some()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// enterprise_rp_id_list() should be non-empty in vendor facilitated mode.
|
||||
if matches!(
|
||||
customization.enterprise_attestation_mode(),
|
||||
Some(EnterpriseAttestationMode::VendorFacilitated)
|
||||
) && customization.enterprise_rp_id_list().is_empty()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// enterprise_rp_id_list() should be empty without an enterprise attestation mode.
|
||||
if customization.enterprise_attestation_mode().is_none()
|
||||
&& !customization.enterprise_rp_id_list().is_empty()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Max pin retries must be less or equal than 8.
|
||||
if customization.max_pin_retries() > 8 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Max cred blob length should be at least 32, and at most 64.
|
||||
if customization.max_cred_blob_length() < 32 || customization.max_cred_blob_length() > 64 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Max credential count in list should be positive if exists.
|
||||
if let Some(count) = customization.max_credential_count_in_list() {
|
||||
if count < 1 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Max large blob array size should not be less than 1024.
|
||||
if customization.max_large_blob_array_size() < 1024 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Default min pin length rp ids must be non-empty if max rp ids length is 0.
|
||||
if customization.max_rp_ids_length() == 0
|
||||
&& customization.default_min_pin_length_rp_ids().is_empty()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_invariants() {
|
||||
assert!(is_valid(&DEFAULT_CUSTOMIZATION));
|
||||
}
|
||||
}
|
||||
20
libraries/opensk/src/api/firmware_protection.rs
Normal file
20
libraries/opensk/src/api/firmware_protection.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright 2022-2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
pub trait FirmwareProtection {
|
||||
/// Locks the firmware.
|
||||
///
|
||||
/// Returns whether the operation was successful.
|
||||
fn lock(&mut self) -> bool;
|
||||
}
|
||||
149
libraries/opensk/src/api/key_store.rs
Normal file
149
libraries/opensk/src/api/key_store.rs
Normal file
@@ -0,0 +1,149 @@
|
||||
// Copyright 2022-2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use crypto::ecdsa::SecKey;
|
||||
use persistent_store::StoreError;
|
||||
use rng256::Rng256;
|
||||
|
||||
use crate::env::Env;
|
||||
|
||||
/// Provides storage for secret keys.
|
||||
///
|
||||
/// Implementations may use the environment store: [`STORAGE_KEY`] is reserved for this usage.
|
||||
pub trait KeyStore {
|
||||
/// Returns the AES key for key handles encryption.
|
||||
fn key_handle_encryption(&mut self) -> Result<[u8; 32], Error>;
|
||||
|
||||
/// Returns the key for key handles authentication.
|
||||
fn key_handle_authentication(&mut self) -> Result<[u8; 32], Error>;
|
||||
|
||||
/// Derives an ECDSA private key from a seed.
|
||||
///
|
||||
/// The result is big-endian.
|
||||
fn derive_ecdsa(&mut self, seed: &[u8; 32]) -> Result<[u8; 32], Error>;
|
||||
|
||||
/// Generates a seed to derive an ECDSA private key.
|
||||
fn generate_ecdsa_seed(&mut self) -> Result<[u8; 32], Error>;
|
||||
|
||||
/// Resets the key store.
|
||||
fn reset(&mut self) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
/// Key store errors.
|
||||
///
|
||||
/// They are deliberately indistinguishable to avoid leaking information.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Error;
|
||||
|
||||
/// Key of the environment store reserved for the key store.
|
||||
pub const STORAGE_KEY: usize = 2046;
|
||||
|
||||
/// Implements a default key store using the environment rng and store.
|
||||
pub trait Helper: Env {}
|
||||
|
||||
impl<T: Helper> KeyStore for T {
|
||||
fn key_handle_encryption(&mut self) -> Result<[u8; 32], Error> {
|
||||
Ok(get_master_keys(self)?.encryption)
|
||||
}
|
||||
|
||||
fn key_handle_authentication(&mut self) -> Result<[u8; 32], Error> {
|
||||
Ok(get_master_keys(self)?.authentication)
|
||||
}
|
||||
|
||||
fn derive_ecdsa(&mut self, seed: &[u8; 32]) -> Result<[u8; 32], Error> {
|
||||
match SecKey::from_bytes(seed) {
|
||||
None => Err(Error),
|
||||
Some(_) => Ok(*seed),
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_ecdsa_seed(&mut self) -> Result<[u8; 32], Error> {
|
||||
let mut seed = [0; 32];
|
||||
SecKey::gensk(self.rng()).to_bytes(&mut seed);
|
||||
Ok(seed)
|
||||
}
|
||||
|
||||
fn reset(&mut self) -> Result<(), Error> {
|
||||
Ok(self.store().remove(STORAGE_KEY)?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper for master keys.
|
||||
struct MasterKeys {
|
||||
/// Master encryption key.
|
||||
encryption: [u8; 32],
|
||||
|
||||
/// Master authentication key.
|
||||
authentication: [u8; 32],
|
||||
}
|
||||
|
||||
fn get_master_keys(env: &mut impl Env) -> Result<MasterKeys, Error> {
|
||||
let master_keys = match env.store().find(STORAGE_KEY)? {
|
||||
Some(x) => x,
|
||||
None => {
|
||||
let master_encryption_key = env.rng().gen_uniform_u8x32();
|
||||
let master_authentication_key = env.rng().gen_uniform_u8x32();
|
||||
let mut master_keys = Vec::with_capacity(64);
|
||||
master_keys.extend_from_slice(&master_encryption_key);
|
||||
master_keys.extend_from_slice(&master_authentication_key);
|
||||
env.store().insert(STORAGE_KEY, &master_keys)?;
|
||||
master_keys
|
||||
}
|
||||
};
|
||||
if master_keys.len() != 64 {
|
||||
return Err(Error);
|
||||
}
|
||||
Ok(MasterKeys {
|
||||
encryption: *array_ref![master_keys, 0, 32],
|
||||
authentication: *array_ref![master_keys, 32, 32],
|
||||
})
|
||||
}
|
||||
|
||||
impl From<StoreError> for Error {
|
||||
fn from(_: StoreError) -> Self {
|
||||
Error
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_key_store() {
|
||||
let mut env = crate::env::test::TestEnv::default();
|
||||
let key_store = env.key_store();
|
||||
|
||||
// Master keys are well-defined and stable.
|
||||
let encryption_key = key_store.key_handle_encryption().unwrap();
|
||||
let authentication_key = key_store.key_handle_authentication().unwrap();
|
||||
assert_eq!(key_store.key_handle_encryption(), Ok(encryption_key));
|
||||
assert_eq!(
|
||||
key_store.key_handle_authentication(),
|
||||
Ok(authentication_key)
|
||||
);
|
||||
|
||||
// ECDSA seeds are well-defined and stable.
|
||||
let ecdsa_seed = key_store.generate_ecdsa_seed().unwrap();
|
||||
let ecdsa_key = key_store.derive_ecdsa(&ecdsa_seed).unwrap();
|
||||
assert_eq!(key_store.derive_ecdsa(&ecdsa_seed), Ok(ecdsa_key));
|
||||
|
||||
// Master keys change after reset. We don't require this for ECDSA seeds because it's not
|
||||
// the case, but it might be better.
|
||||
key_store.reset().unwrap();
|
||||
assert!(key_store.key_handle_encryption().unwrap() != encryption_key);
|
||||
assert!(key_store.key_handle_authentication().unwrap() != authentication_key);
|
||||
}
|
||||
}
|
||||
27
libraries/opensk/src/api/mod.rs
Normal file
27
libraries/opensk/src/api/mod.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright 2022-2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! APIs for the environment.
|
||||
//!
|
||||
//! The [environment](crate::env::Env) is split into components. Each component has an API described
|
||||
//! by a trait. This module gathers the API of those components.
|
||||
|
||||
pub mod attestation_store;
|
||||
pub mod clock;
|
||||
pub mod connection;
|
||||
pub mod customization;
|
||||
pub mod firmware_protection;
|
||||
pub mod key_store;
|
||||
pub mod upgrade_storage;
|
||||
pub mod user_presence;
|
||||
384
libraries/opensk/src/api/upgrade_storage/helper.rs
Normal file
384
libraries/opensk/src/api/upgrade_storage/helper.rs
Normal file
@@ -0,0 +1,384 @@
|
||||
// Copyright 2019-2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// For compiling with std outside of tests.
|
||||
#![cfg_attr(feature = "std", allow(dead_code))]
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use core::iter::Iterator;
|
||||
use persistent_store::{StorageError, StorageResult};
|
||||
|
||||
/// Reads a slice from a list of slices.
|
||||
///
|
||||
/// The returned slice contains the interval `[start, start+length)`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`StorageError::OutOfBounds`] if the range is not within exactly one slice.
|
||||
pub fn find_slice<'a>(
|
||||
slices: &'a [&'a [u8]],
|
||||
mut start: usize,
|
||||
length: usize,
|
||||
) -> StorageResult<&'a [u8]> {
|
||||
for slice in slices {
|
||||
if start >= slice.len() {
|
||||
start -= slice.len();
|
||||
continue;
|
||||
}
|
||||
if start + length > slice.len() {
|
||||
break;
|
||||
}
|
||||
return Ok(&slice[start..][..length]);
|
||||
}
|
||||
Err(StorageError::OutOfBounds)
|
||||
}
|
||||
|
||||
/// Checks whether the address is aligned with the block size.
|
||||
///
|
||||
/// Requires `block_size` to be a power of two.
|
||||
pub fn is_aligned(block_size: usize, address: usize) -> bool {
|
||||
debug_assert!(block_size.is_power_of_two());
|
||||
address & (block_size - 1) == 0
|
||||
}
|
||||
|
||||
/// A range implementation using start and length.
|
||||
///
|
||||
/// The range is treated as the interval `[start, start + length)`.
|
||||
/// All objects with length of 0, regardless of the start value, are considered empty.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ModRange {
|
||||
start: usize,
|
||||
length: usize,
|
||||
}
|
||||
|
||||
impl ModRange {
|
||||
/// Returns a new range of given start and length.
|
||||
pub fn new(start: usize, length: usize) -> ModRange {
|
||||
ModRange { start, length }
|
||||
}
|
||||
|
||||
/// Create a new empty range.
|
||||
pub fn new_empty() -> ModRange {
|
||||
ModRange::new(0, 0)
|
||||
}
|
||||
|
||||
/// Returns the start of the range.
|
||||
pub fn start(&self) -> usize {
|
||||
self.start
|
||||
}
|
||||
|
||||
/// Returns the length of the range.
|
||||
pub fn length(&self) -> usize {
|
||||
self.length
|
||||
}
|
||||
|
||||
/// Returns whether this range contains any addresses.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.length == 0
|
||||
}
|
||||
|
||||
/// Returns the disjoint union with the other range, if consecutive.
|
||||
///
|
||||
/// Appending empty ranges is not possible.
|
||||
/// Appending to the empty range returns the other range.
|
||||
///
|
||||
/// Returns true if successful.
|
||||
pub fn append(&mut self, other: &ModRange) -> bool {
|
||||
if self.is_empty() {
|
||||
self.start = other.start;
|
||||
self.length = other.length;
|
||||
return true;
|
||||
}
|
||||
if other.is_empty() {
|
||||
return false;
|
||||
}
|
||||
if self.start >= other.start {
|
||||
return false;
|
||||
}
|
||||
if self.length != other.start - self.start {
|
||||
return false;
|
||||
}
|
||||
if let Some(new_length) = self.length.checked_add(other.length) {
|
||||
self.length = new_length;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to check whether a range starts within another.
|
||||
fn starts_inside(&self, range: &ModRange) -> bool {
|
||||
!range.is_empty() && self.start >= range.start && self.start - range.start < range.length
|
||||
}
|
||||
|
||||
/// Returns whether the given range has intersects.
|
||||
///
|
||||
/// Mathematically, we calculate whether: `self ∩ range ≠ ∅`.
|
||||
pub fn intersects_range(&self, range: &ModRange) -> bool {
|
||||
self.starts_inside(range) || range.starts_inside(self)
|
||||
}
|
||||
|
||||
/// Returns whether the given range is fully contained.
|
||||
///
|
||||
/// Mathematically, we calculate whether: `self ∩ range = range`.
|
||||
pub fn contains_range(&self, range: &ModRange) -> bool {
|
||||
range.is_empty()
|
||||
|| (self.start <= range.start
|
||||
&& range.length <= self.length
|
||||
&& range.start - self.start <= self.length - range.length)
|
||||
}
|
||||
|
||||
/// Returns an iterator for all contained numbers that are divisible by the modulus.
|
||||
pub fn aligned_iter(&self, modulus: usize) -> impl Iterator<Item = usize> {
|
||||
(self.start..=usize::MAX)
|
||||
.take(self.length)
|
||||
// Skip the minimum number of elements to align.
|
||||
.skip((modulus - self.start % modulus) % modulus)
|
||||
// Only return aligned elements.
|
||||
.step_by(modulus)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Partition {
|
||||
ranges: Vec<ModRange>,
|
||||
}
|
||||
|
||||
impl Partition {
|
||||
pub fn new() -> Partition {
|
||||
Partition { ranges: Vec::new() }
|
||||
}
|
||||
|
||||
/// Total length of all ranges.
|
||||
pub fn length(&self) -> usize {
|
||||
self.ranges.iter().map(|r| r.length()).sum()
|
||||
}
|
||||
|
||||
/// Appends the given range.
|
||||
///
|
||||
/// Ranges should be appending with ascending start addresses.
|
||||
pub fn append(&mut self, range: ModRange) -> bool {
|
||||
if let Some(last_range) = self.ranges.last_mut() {
|
||||
if range.start() <= last_range.start()
|
||||
|| range.start() - last_range.start() < last_range.length()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if !last_range.append(&range) {
|
||||
self.ranges.push(range);
|
||||
}
|
||||
} else {
|
||||
self.ranges.push(range);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Returns the start address that corresponds to the given offset.
|
||||
///
|
||||
/// If the offset bigger than the accumulated length or the requested slice doesn't fit a
|
||||
/// connected component, return `None`.
|
||||
pub fn find_address(&self, mut offset: usize, length: usize) -> Option<usize> {
|
||||
for range in &self.ranges {
|
||||
if offset < range.length() {
|
||||
return if range.length() - offset >= length {
|
||||
Some(range.start() + offset)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
offset -= range.length()
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn ranges_from(&self, start_address: usize) -> Vec<ModRange> {
|
||||
let mut result = Vec::new();
|
||||
for range in &self.ranges {
|
||||
match start_address.checked_sub(range.start()) {
|
||||
None | Some(0) => result.push(range.clone()),
|
||||
Some(offset) => {
|
||||
if range.length() > offset {
|
||||
result.push(ModRange::new(start_address, range.length() - offset));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn find_slice_ok() {
|
||||
assert_eq!(
|
||||
find_slice(&[&[1, 2, 3, 4]], 0, 4).ok(),
|
||||
Some(&[1u8, 2, 3, 4] as &[u8])
|
||||
);
|
||||
assert_eq!(
|
||||
find_slice(&[&[1, 2, 3, 4], &[5, 6]], 1, 2).ok(),
|
||||
Some(&[2u8, 3] as &[u8])
|
||||
);
|
||||
assert_eq!(
|
||||
find_slice(&[&[1, 2, 3, 4], &[5, 6]], 4, 2).ok(),
|
||||
Some(&[5u8, 6] as &[u8])
|
||||
);
|
||||
assert_eq!(
|
||||
find_slice(&[&[1, 2, 3, 4], &[5, 6]], 4, 0).ok(),
|
||||
Some(&[] as &[u8])
|
||||
);
|
||||
assert!(find_slice(&[], 0, 1).is_err());
|
||||
assert!(find_slice(&[&[1, 2, 3, 4], &[5, 6]], 6, 0).is_err());
|
||||
assert!(find_slice(&[&[1, 2, 3, 4], &[5, 6]], 3, 2).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alignment() {
|
||||
for exponent in 0..8 {
|
||||
let block_size = 1 << exponent;
|
||||
for i in 0..10 {
|
||||
assert!(is_aligned(block_size, block_size * i));
|
||||
}
|
||||
for i in 1..block_size {
|
||||
assert!(!is_aligned(block_size, block_size + i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mod_range_parameters() {
|
||||
let range = ModRange::new(200, 100);
|
||||
assert_eq!(range.start(), 200);
|
||||
assert_eq!(range.length(), 100);
|
||||
assert_eq!(ModRange::new_empty().length(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mod_range_is_empty() {
|
||||
assert!(!ModRange::new(200, 100).is_empty());
|
||||
assert!(ModRange::new(200, 0).is_empty());
|
||||
assert!(ModRange::new_empty().is_empty());
|
||||
assert!(!ModRange::new(usize::MAX, 2).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mod_range_append() {
|
||||
let mut range = ModRange::new(200, 100);
|
||||
assert!(range.append(&ModRange::new(300, 400)));
|
||||
assert!(range.start() == 200);
|
||||
assert!(range.length() == 500);
|
||||
assert!(!range.append(&ModRange::new(499, 400)));
|
||||
assert!(!range.append(&ModRange::new(501, 400)));
|
||||
assert!(!range.append(&ModRange::new(300, 400)));
|
||||
let mut range = ModRange::new_empty();
|
||||
assert!(range.append(&ModRange::new(200, 100)));
|
||||
assert!(range.start() == 200);
|
||||
assert!(range.length() == 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mod_range_contains_range() {
|
||||
let range = ModRange::new(200, 100);
|
||||
assert!(!range.contains_range(&ModRange::new(199, 100)));
|
||||
assert!(!range.contains_range(&ModRange::new(201, 100)));
|
||||
assert!(!range.contains_range(&ModRange::new(199, 99)));
|
||||
assert!(!range.contains_range(&ModRange::new(202, 99)));
|
||||
assert!(!range.contains_range(&ModRange::new(200, 101)));
|
||||
assert!(range.contains_range(&ModRange::new(200, 100)));
|
||||
assert!(range.contains_range(&ModRange::new(200, 99)));
|
||||
assert!(range.contains_range(&ModRange::new(201, 99)));
|
||||
assert!(ModRange::new_empty().contains_range(&ModRange::new_empty()));
|
||||
assert!(ModRange::new(usize::MAX, 1).contains_range(&ModRange::new(usize::MAX, 1)));
|
||||
assert!(ModRange::new(usize::MAX, 2).contains_range(&ModRange::new(usize::MAX, 2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mod_range_intersects_range() {
|
||||
let range = ModRange::new(200, 100);
|
||||
assert!(range.intersects_range(&ModRange::new(200, 1)));
|
||||
assert!(range.intersects_range(&ModRange::new(299, 1)));
|
||||
assert!(!range.intersects_range(&ModRange::new(199, 1)));
|
||||
assert!(!range.intersects_range(&ModRange::new(300, 1)));
|
||||
assert!(!ModRange::new_empty().intersects_range(&ModRange::new_empty()));
|
||||
assert!(!ModRange::new_empty().intersects_range(&ModRange::new(200, 100)));
|
||||
assert!(!ModRange::new(200, 100).intersects_range(&ModRange::new_empty()));
|
||||
assert!(ModRange::new(usize::MAX, 1).intersects_range(&ModRange::new(usize::MAX, 1)));
|
||||
assert!(ModRange::new(usize::MAX, 2).intersects_range(&ModRange::new(usize::MAX, 2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mod_range_aligned_iter() {
|
||||
let mut iter = ModRange::new(200, 100).aligned_iter(100);
|
||||
assert_eq!(iter.next(), Some(200));
|
||||
assert_eq!(iter.next(), None);
|
||||
let mut iter = ModRange::new(200, 101).aligned_iter(100);
|
||||
assert_eq!(iter.next(), Some(200));
|
||||
assert_eq!(iter.next(), Some(300));
|
||||
assert_eq!(iter.next(), None);
|
||||
let mut iter = ModRange::new(199, 100).aligned_iter(100);
|
||||
assert_eq!(iter.next(), Some(200));
|
||||
assert_eq!(iter.next(), None);
|
||||
let mut iter = ModRange::new(201, 99).aligned_iter(100);
|
||||
assert_eq!(iter.next(), None);
|
||||
let mut iter = ModRange::new(usize::MAX - 16, 20).aligned_iter(16);
|
||||
assert_eq!(iter.next(), Some(0xffff_ffff_ffff_fff0));
|
||||
assert_eq!(iter.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partition_append() {
|
||||
let mut partition = Partition::new();
|
||||
partition.append(ModRange::new(0x4000, 0x1000));
|
||||
partition.append(ModRange::new(0x20000, 0x20000));
|
||||
partition.append(ModRange::new(0x40000, 0x20000));
|
||||
assert_eq!(partition.find_address(0, 1), Some(0x4000));
|
||||
assert_eq!(partition.length(), 0x41000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partition_find_address() {
|
||||
let mut partition = Partition::new();
|
||||
partition.append(ModRange::new(0x4000, 0x1000));
|
||||
partition.append(ModRange::new(0x20000, 0x20000));
|
||||
partition.append(ModRange::new(0x40000, 0x20000));
|
||||
assert_eq!(partition.find_address(0, 0x1000), Some(0x4000));
|
||||
assert_eq!(partition.find_address(0x1000, 0x1000), Some(0x20000));
|
||||
assert_eq!(partition.find_address(0x20000, 0x1000), Some(0x3F000));
|
||||
assert_eq!(partition.find_address(0x21000, 0x1000), Some(0x40000));
|
||||
assert_eq!(partition.find_address(0x40000, 0x1000), Some(0x5F000));
|
||||
assert_eq!(partition.find_address(0x41000, 0x1000), None);
|
||||
assert_eq!(partition.find_address(0x40000, 0x2000), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partition_ranges_from() {
|
||||
let mut partition = Partition::new();
|
||||
partition.append(ModRange::new(0x4000, 0x1000));
|
||||
partition.append(ModRange::new(0x20000, 0x20000));
|
||||
partition.append(ModRange::new(0x40000, 0x20000));
|
||||
let all_ranges = partition.ranges_from(0);
|
||||
let from_start_ranges = partition.ranges_from(0x4000);
|
||||
assert_eq!(&all_ranges, &from_start_ranges);
|
||||
assert_eq!(all_ranges.len(), 2);
|
||||
assert_eq!(all_ranges[0], ModRange::new(0x4000, 0x1000));
|
||||
assert_eq!(all_ranges[1], ModRange::new(0x20000, 0x40000));
|
||||
let second_range = partition.ranges_from(0x20000);
|
||||
let same_second_range = partition.ranges_from(0x1F000);
|
||||
assert_eq!(&second_range, &same_second_range);
|
||||
assert_eq!(&second_range, &all_ranges[1..]);
|
||||
let partial_range = partition.ranges_from(0x30000);
|
||||
assert_eq!(partial_range[0], ModRange::new(0x30000, 0x30000));
|
||||
}
|
||||
}
|
||||
39
libraries/opensk/src/api/upgrade_storage/mod.rs
Normal file
39
libraries/opensk/src/api/upgrade_storage/mod.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright 2021-2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use persistent_store::StorageResult;
|
||||
|
||||
pub mod helper;
|
||||
|
||||
/// Accessors to storage locations used for upgrading from a CTAP command.
|
||||
pub trait UpgradeStorage {
|
||||
/// Processes the given data as part of an upgrade.
|
||||
///
|
||||
/// The offset indicates the data location inside the bundle.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - Returns [`StorageError::OutOfBounds`] if the data does not fit.
|
||||
/// - Returns [`StorageError::CustomError`] if any Metadata or other check fails.
|
||||
fn write_bundle(&mut self, offset: usize, data: Vec<u8>) -> StorageResult<()>;
|
||||
|
||||
/// Returns an identifier for the requested bundle.
|
||||
///
|
||||
/// Use this to determine whether you are writing to A or B.
|
||||
fn bundle_identifier(&self) -> u32;
|
||||
|
||||
/// Returns the currently running firmware version.
|
||||
fn running_firmware_version(&self) -> u64;
|
||||
}
|
||||
42
libraries/opensk/src/api/user_presence.rs
Normal file
42
libraries/opensk/src/api/user_presence.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright 2022-2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum UserPresenceError {
|
||||
/// User explicitly declined user presence check.
|
||||
Declined,
|
||||
/// User presence check was canceled by User Agent.
|
||||
Canceled,
|
||||
/// User presence check timed out.
|
||||
Timeout,
|
||||
}
|
||||
|
||||
pub type UserPresenceResult = Result<(), UserPresenceError>;
|
||||
|
||||
pub trait UserPresence {
|
||||
/// Initializes for a user presence check.
|
||||
///
|
||||
/// Must eventually be followed by a call to [`Self::check_complete`].
|
||||
fn check_init(&mut self);
|
||||
|
||||
/// Waits until user presence is confirmed, rejected, or the given timeout expires.
|
||||
///
|
||||
/// Must be called between calls to [`Self::check_init`] and [`Self::check_complete`].
|
||||
fn wait_with_timeout(&mut self, timeout_ms: usize) -> UserPresenceResult;
|
||||
|
||||
/// Finalizes a user presence check.
|
||||
///
|
||||
/// Must be called after [`Self::check_init`].
|
||||
fn check_complete(&mut self);
|
||||
}
|
||||
449
libraries/opensk/src/ctap/apdu.rs
Normal file
449
libraries/opensk/src/ctap/apdu.rs
Normal file
@@ -0,0 +1,449 @@
|
||||
// Copyright 2020-2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use byteorder::{BigEndian, ByteOrder};
|
||||
use core::convert::TryFrom;
|
||||
|
||||
use crate::api::attestation_store;
|
||||
|
||||
const APDU_HEADER_LEN: usize = 4;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[allow(non_camel_case_types, dead_code)]
|
||||
pub enum ApduStatusCode {
|
||||
SW_SUCCESS = 0x90_00,
|
||||
/// Command successfully executed; 'XX' bytes of data are
|
||||
/// available and can be requested using GET RESPONSE.
|
||||
SW_GET_RESPONSE = 0x61_00,
|
||||
SW_MEMERR = 0x65_01,
|
||||
SW_WRONG_DATA = 0x6a_80,
|
||||
SW_WRONG_LENGTH = 0x67_00,
|
||||
SW_COND_USE_NOT_SATISFIED = 0x69_85,
|
||||
SW_COMMAND_NOT_ALLOWED = 0x69_86,
|
||||
SW_FILE_NOT_FOUND = 0x6a_82,
|
||||
SW_INCORRECT_P1P2 = 0x6a_86,
|
||||
/// Instruction code not supported or invalid
|
||||
SW_INS_INVALID = 0x6d_00,
|
||||
SW_CLA_INVALID = 0x6e_00,
|
||||
SW_INTERNAL_EXCEPTION = 0x6f_00,
|
||||
}
|
||||
|
||||
impl From<ApduStatusCode> for u16 {
|
||||
fn from(code: ApduStatusCode) -> Self {
|
||||
code as u16
|
||||
}
|
||||
}
|
||||
|
||||
impl From<attestation_store::Error> for ApduStatusCode {
|
||||
fn from(error: attestation_store::Error) -> Self {
|
||||
use attestation_store::Error;
|
||||
match error {
|
||||
Error::Storage => ApduStatusCode::SW_MEMERR,
|
||||
Error::Internal => ApduStatusCode::SW_INTERNAL_EXCEPTION,
|
||||
Error::NoSupport => ApduStatusCode::SW_INTERNAL_EXCEPTION,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub enum ApduInstructions {
|
||||
Select = 0xA4,
|
||||
ReadBinary = 0xB0,
|
||||
GetResponse = 0xC0,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[allow(dead_code)]
|
||||
pub struct ApduHeader {
|
||||
pub cla: u8,
|
||||
pub ins: u8,
|
||||
pub p1: u8,
|
||||
pub p2: u8,
|
||||
}
|
||||
|
||||
impl From<&[u8; APDU_HEADER_LEN]> for ApduHeader {
|
||||
fn from(header: &[u8; APDU_HEADER_LEN]) -> Self {
|
||||
ApduHeader {
|
||||
cla: header[0],
|
||||
ins: header[1],
|
||||
p1: header[2],
|
||||
p2: header[3],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
/// The APDU cases
|
||||
pub enum Case {
|
||||
Le1,
|
||||
Lc1Data,
|
||||
Lc1DataLe1,
|
||||
Lc3Data,
|
||||
Lc3DataLe1,
|
||||
Lc3DataLe2,
|
||||
Le3,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[allow(dead_code)]
|
||||
pub enum ApduType {
|
||||
Instruction,
|
||||
Short(Case),
|
||||
Extended(Case),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[allow(dead_code)]
|
||||
pub struct Apdu {
|
||||
pub header: ApduHeader,
|
||||
pub lc: u16,
|
||||
pub data: Vec<u8>,
|
||||
pub le: u32,
|
||||
pub case_type: ApduType,
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for Apdu {
|
||||
type Error = ApduStatusCode;
|
||||
|
||||
fn try_from(frame: &[u8]) -> Result<Self, ApduStatusCode> {
|
||||
if frame.len() < APDU_HEADER_LEN as usize {
|
||||
return Err(ApduStatusCode::SW_WRONG_DATA);
|
||||
}
|
||||
// +-----+-----+----+----+
|
||||
// header | CLA | INS | P1 | P2 |
|
||||
// +-----+-----+----+----+
|
||||
let (header, payload) = frame.split_at(APDU_HEADER_LEN);
|
||||
|
||||
if payload.is_empty() {
|
||||
// Lc is zero-bytes in length
|
||||
return Ok(Apdu {
|
||||
header: array_ref!(header, 0, APDU_HEADER_LEN).into(),
|
||||
lc: 0x00,
|
||||
data: Vec::new(),
|
||||
le: 0x00,
|
||||
case_type: ApduType::Instruction,
|
||||
});
|
||||
}
|
||||
// Lc is not zero-bytes in length, let's figure out how long it is
|
||||
let byte_0 = payload[0];
|
||||
if payload.len() == 1 {
|
||||
// There is only one byte in the payload, that byte cannot be Lc because that would
|
||||
// entail at *least* one another byte in the payload (for the command data)
|
||||
return Ok(Apdu {
|
||||
header: array_ref!(header, 0, APDU_HEADER_LEN).into(),
|
||||
lc: 0x00,
|
||||
data: Vec::new(),
|
||||
le: if byte_0 == 0x00 {
|
||||
// Ne = 256
|
||||
0x100
|
||||
} else {
|
||||
byte_0.into()
|
||||
},
|
||||
case_type: ApduType::Short(Case::Le1),
|
||||
});
|
||||
}
|
||||
if payload.len() == 1 + (byte_0 as usize) && byte_0 != 0 {
|
||||
// Lc is one-byte long and since the size specified by Lc covers the rest of the
|
||||
// payload there's no Le at the end
|
||||
return Ok(Apdu {
|
||||
header: array_ref!(header, 0, APDU_HEADER_LEN).into(),
|
||||
lc: byte_0.into(),
|
||||
data: payload[1..].to_vec(),
|
||||
case_type: ApduType::Short(Case::Lc1Data),
|
||||
le: 0,
|
||||
});
|
||||
}
|
||||
if payload.len() == 2 + (byte_0 as usize) && byte_0 != 0 {
|
||||
// Lc is one-byte long and since the size specified by Lc covers the rest of the
|
||||
// payload with ONE additional byte that byte must be Le
|
||||
let last_byte: u32 = (*payload.last().unwrap()).into();
|
||||
return Ok(Apdu {
|
||||
header: array_ref!(header, 0, APDU_HEADER_LEN).into(),
|
||||
lc: byte_0.into(),
|
||||
data: payload[1..(payload.len() - 1)].to_vec(),
|
||||
le: if last_byte == 0x00 { 0x100 } else { last_byte },
|
||||
case_type: ApduType::Short(Case::Lc1DataLe1),
|
||||
});
|
||||
}
|
||||
if payload.len() > 2 {
|
||||
// Lc is possibly three-bytes long
|
||||
let extended_apdu_lc = BigEndian::read_u16(&payload[1..3]) as usize;
|
||||
if payload.len() < extended_apdu_lc + 3 {
|
||||
return Err(ApduStatusCode::SW_WRONG_LENGTH);
|
||||
}
|
||||
|
||||
let extended_apdu_le_len: usize = payload
|
||||
.len()
|
||||
.checked_sub(extended_apdu_lc + 3)
|
||||
.ok_or(ApduStatusCode::SW_WRONG_LENGTH)?;
|
||||
if extended_apdu_le_len > 3 {
|
||||
return Err(ApduStatusCode::SW_WRONG_LENGTH);
|
||||
}
|
||||
|
||||
if byte_0 == 0 && extended_apdu_le_len <= 3 {
|
||||
// If first byte is zero AND the next two bytes can be parsed as a big-endian
|
||||
// length that covers the rest of the block (plus few additional bytes for Le), we
|
||||
// have an extended-length APDU
|
||||
let last_byte: u32 = (*payload.last().unwrap()).into();
|
||||
return Ok(Apdu {
|
||||
header: array_ref!(header, 0, APDU_HEADER_LEN).into(),
|
||||
lc: extended_apdu_lc as u16,
|
||||
data: payload[3..(payload.len() - extended_apdu_le_len)].to_vec(),
|
||||
le: match extended_apdu_le_len {
|
||||
0 => 0,
|
||||
1 => {
|
||||
if last_byte == 0x00 {
|
||||
0x100
|
||||
} else {
|
||||
last_byte
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
let le_parsed = BigEndian::read_u16(&payload[payload.len() - 2..]);
|
||||
if le_parsed == 0x00 {
|
||||
0x10000
|
||||
} else {
|
||||
le_parsed as u32
|
||||
}
|
||||
}
|
||||
3 => {
|
||||
let le_first_byte: u32 =
|
||||
(*payload.get(payload.len() - 3).unwrap()).into();
|
||||
if le_first_byte != 0x00 {
|
||||
return Err(ApduStatusCode::SW_INTERNAL_EXCEPTION);
|
||||
}
|
||||
let le_parsed = BigEndian::read_u16(&payload[payload.len() - 2..]);
|
||||
if le_parsed == 0x00 {
|
||||
0x10000
|
||||
} else {
|
||||
le_parsed as u32
|
||||
}
|
||||
}
|
||||
_ => return Err(ApduStatusCode::SW_INTERNAL_EXCEPTION),
|
||||
},
|
||||
case_type: ApduType::Extended(match extended_apdu_le_len {
|
||||
0 => Case::Lc3Data,
|
||||
1 => Case::Lc3DataLe1,
|
||||
2 => Case::Lc3DataLe2,
|
||||
3 => Case::Le3,
|
||||
_ => return Err(ApduStatusCode::SW_INTERNAL_EXCEPTION),
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Err(ApduStatusCode::SW_INTERNAL_EXCEPTION)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
fn pass_frame(frame: &[u8]) -> Result<Apdu, ApduStatusCode> {
|
||||
Apdu::try_from(frame)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_case_type_1() {
|
||||
let frame: [u8; 4] = [0x00, 0x12, 0x00, 0x80];
|
||||
let response = pass_frame(&frame);
|
||||
assert!(response.is_ok());
|
||||
let expected = Apdu {
|
||||
header: ApduHeader {
|
||||
cla: 0x00,
|
||||
ins: 0x12,
|
||||
p1: 0x00,
|
||||
p2: 0x80,
|
||||
},
|
||||
lc: 0x00,
|
||||
data: Vec::new(),
|
||||
le: 0x00,
|
||||
case_type: ApduType::Instruction,
|
||||
};
|
||||
assert_eq!(Ok(expected), response);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_case_type_2_short() {
|
||||
let frame: [u8; 5] = [0x00, 0xb0, 0x00, 0x00, 0x0f];
|
||||
let response = pass_frame(&frame);
|
||||
let expected = Apdu {
|
||||
header: ApduHeader {
|
||||
cla: 0x00,
|
||||
ins: 0xb0,
|
||||
p1: 0x00,
|
||||
p2: 0x00,
|
||||
},
|
||||
lc: 0x00,
|
||||
data: Vec::new(),
|
||||
le: 0x0f,
|
||||
case_type: ApduType::Short(Case::Le1),
|
||||
};
|
||||
assert_eq!(Ok(expected), response);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_case_type_2_short_le() {
|
||||
let frame: [u8; 5] = [0x00, 0xb0, 0x00, 0x00, 0x00];
|
||||
let response = pass_frame(&frame);
|
||||
let expected = Apdu {
|
||||
header: ApduHeader {
|
||||
cla: 0x00,
|
||||
ins: 0xb0,
|
||||
p1: 0x00,
|
||||
p2: 0x00,
|
||||
},
|
||||
lc: 0x00,
|
||||
data: Vec::new(),
|
||||
le: 0x100,
|
||||
case_type: ApduType::Short(Case::Le1),
|
||||
};
|
||||
assert_eq!(Ok(expected), response);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_case_type_3_short() {
|
||||
let frame: [u8; 7] = [0x00, 0xa4, 0x00, 0x0c, 0x02, 0xe1, 0x04];
|
||||
let payload = [0xe1, 0x04];
|
||||
let response = pass_frame(&frame);
|
||||
let expected = Apdu {
|
||||
header: ApduHeader {
|
||||
cla: 0x00,
|
||||
ins: 0xa4,
|
||||
p1: 0x00,
|
||||
p2: 0x0c,
|
||||
},
|
||||
lc: 0x02,
|
||||
data: payload.to_vec(),
|
||||
le: 0x00,
|
||||
case_type: ApduType::Short(Case::Lc1Data),
|
||||
};
|
||||
assert_eq!(Ok(expected), response);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_case_type_4_short() {
|
||||
let frame: [u8; 13] = [
|
||||
0x00, 0xa4, 0x04, 0x00, 0x07, 0xd2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01, 0xff,
|
||||
];
|
||||
let payload = [0xd2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01];
|
||||
let response = pass_frame(&frame);
|
||||
let expected = Apdu {
|
||||
header: ApduHeader {
|
||||
cla: 0x00,
|
||||
ins: 0xa4,
|
||||
p1: 0x04,
|
||||
p2: 0x00,
|
||||
},
|
||||
lc: 0x07,
|
||||
data: payload.to_vec(),
|
||||
le: 0xff,
|
||||
case_type: ApduType::Short(Case::Lc1DataLe1),
|
||||
};
|
||||
assert_eq!(Ok(expected), response);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_case_type_4_short_le() {
|
||||
let frame: [u8; 13] = [
|
||||
0x00, 0xa4, 0x04, 0x00, 0x07, 0xd2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01, 0x00,
|
||||
];
|
||||
let payload = [0xd2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01];
|
||||
let response = pass_frame(&frame);
|
||||
let expected = Apdu {
|
||||
header: ApduHeader {
|
||||
cla: 0x00,
|
||||
ins: 0xa4,
|
||||
p1: 0x04,
|
||||
p2: 0x00,
|
||||
},
|
||||
lc: 0x07,
|
||||
data: payload.to_vec(),
|
||||
le: 0x100,
|
||||
case_type: ApduType::Short(Case::Lc1DataLe1),
|
||||
};
|
||||
assert_eq!(Ok(expected), response);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_apdu_header_length() {
|
||||
let frame: [u8; 3] = [0x00, 0x12, 0x00];
|
||||
let response = pass_frame(&frame);
|
||||
assert_eq!(Err(ApduStatusCode::SW_WRONG_DATA), response);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extended_length_apdu() {
|
||||
let frame: [u8; 186] = [
|
||||
0x00, 0x02, 0x03, 0x00, 0x00, 0x00, 0xb1, 0x60, 0xc5, 0xb3, 0x42, 0x58, 0x6b, 0x49,
|
||||
0xdb, 0x3e, 0x72, 0xd8, 0x24, 0x4b, 0xa5, 0x6c, 0x8d, 0x79, 0x2b, 0x65, 0x08, 0xe8,
|
||||
0xda, 0x9b, 0x0e, 0x2b, 0xc1, 0x63, 0x0d, 0xbc, 0xf3, 0x6d, 0x66, 0xa5, 0x46, 0x72,
|
||||
0xb2, 0x22, 0xc4, 0xcf, 0x95, 0xe1, 0x51, 0xed, 0x8d, 0x4d, 0x3c, 0x76, 0x7a, 0x6c,
|
||||
0xc3, 0x49, 0x43, 0x59, 0x43, 0x79, 0x4e, 0x88, 0x4f, 0x3d, 0x02, 0x3a, 0x82, 0x29,
|
||||
0xfd, 0x70, 0x3f, 0x8b, 0xd4, 0xff, 0xe0, 0xa8, 0x93, 0xdf, 0x1a, 0x58, 0x34, 0x16,
|
||||
0xb0, 0x1b, 0x8e, 0xbc, 0xf0, 0x2d, 0xc9, 0x99, 0x8d, 0x6f, 0xe4, 0x8a, 0xb2, 0x70,
|
||||
0x9a, 0x70, 0x3a, 0x27, 0x71, 0x88, 0x3c, 0x75, 0x30, 0x16, 0xfb, 0x02, 0x11, 0x4d,
|
||||
0x30, 0x54, 0x6c, 0x4e, 0x8c, 0x76, 0xb2, 0xf0, 0xa8, 0x4e, 0xd6, 0x90, 0xe4, 0x40,
|
||||
0x25, 0x6a, 0xdd, 0x64, 0x63, 0x3e, 0x83, 0x4f, 0x8b, 0x25, 0xcf, 0x88, 0x68, 0x80,
|
||||
0x01, 0x07, 0xdb, 0xc8, 0x64, 0xf7, 0xca, 0x4f, 0xd1, 0xc7, 0x95, 0x7c, 0xe8, 0x45,
|
||||
0xbc, 0xda, 0xd4, 0xef, 0x45, 0x63, 0x5a, 0x7a, 0x65, 0x3f, 0xaa, 0x22, 0x67, 0xe7,
|
||||
0x8a, 0xf2, 0x5f, 0xe8, 0x59, 0x2e, 0x0b, 0xc6, 0x85, 0xc6, 0xf7, 0x0e, 0x9e, 0xdb,
|
||||
0xb6, 0x2b, 0x00, 0x00,
|
||||
];
|
||||
let payload: &[u8] = &frame[7..frame.len() - 2];
|
||||
let response = pass_frame(&frame);
|
||||
let expected = Apdu {
|
||||
header: ApduHeader {
|
||||
cla: 0x00,
|
||||
ins: 0x02,
|
||||
p1: 0x03,
|
||||
p2: 0x00,
|
||||
},
|
||||
lc: 0xb1,
|
||||
data: payload.to_vec(),
|
||||
le: 0x10000,
|
||||
case_type: ApduType::Extended(Case::Lc3DataLe2),
|
||||
};
|
||||
assert_eq!(Ok(expected), response);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_previously_unsupported_case_type() {
|
||||
let frame: [u8; 73] = [
|
||||
0x00, 0x01, 0x03, 0x00, 0x00, 0x00, 0x40, 0xe3, 0x8f, 0xde, 0x51, 0x3d, 0xac, 0x9d,
|
||||
0x1c, 0x6e, 0x86, 0x76, 0x31, 0x40, 0x25, 0x96, 0x86, 0x4d, 0x29, 0xe8, 0x07, 0xb3,
|
||||
0x56, 0x19, 0xdf, 0x4a, 0x00, 0x02, 0xae, 0x2a, 0x8c, 0x9d, 0x5a, 0xab, 0xc3, 0x4b,
|
||||
0x4e, 0xb9, 0x78, 0xb9, 0x11, 0xe5, 0x52, 0x40, 0xf3, 0x45, 0x64, 0x9c, 0xd3, 0xd7,
|
||||
0xe8, 0xb5, 0x83, 0xfb, 0xe0, 0x66, 0x98, 0x4d, 0x98, 0x81, 0xf7, 0xb5, 0x49, 0x4d,
|
||||
0xcb, 0x00, 0x00,
|
||||
];
|
||||
let payload: &[u8] = &frame[7..frame.len() - 2];
|
||||
let response = pass_frame(&frame);
|
||||
let expected = Apdu {
|
||||
header: ApduHeader {
|
||||
cla: 0x00,
|
||||
ins: 0x01,
|
||||
p1: 0x03,
|
||||
p2: 0x00,
|
||||
},
|
||||
lc: 0x40,
|
||||
data: payload.to_vec(),
|
||||
le: 0x10000,
|
||||
case_type: ApduType::Extended(Case::Lc3DataLe2),
|
||||
};
|
||||
assert_eq!(Ok(expected), response);
|
||||
}
|
||||
}
|
||||
1704
libraries/opensk/src/ctap/client_pin.rs
Normal file
1704
libraries/opensk/src/ctap/client_pin.rs
Normal file
File diff suppressed because it is too large
Load Diff
1160
libraries/opensk/src/ctap/command.rs
Normal file
1160
libraries/opensk/src/ctap/command.rs
Normal file
File diff suppressed because it is too large
Load Diff
489
libraries/opensk/src/ctap/config_command.rs
Normal file
489
libraries/opensk/src/ctap/config_command.rs
Normal file
@@ -0,0 +1,489 @@
|
||||
// Copyright 2020-2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::client_pin::{ClientPin, PinPermission};
|
||||
use super::command::AuthenticatorConfigParameters;
|
||||
use super::data_formats::{ConfigSubCommand, ConfigSubCommandParams, SetMinPinLengthParams};
|
||||
use super::response::ResponseData;
|
||||
use super::status_code::Ctap2StatusCode;
|
||||
use crate::api::customization::Customization;
|
||||
use crate::ctap::storage;
|
||||
use crate::env::Env;
|
||||
use alloc::vec;
|
||||
|
||||
/// Processes the subcommand enableEnterpriseAttestation for AuthenticatorConfig.
|
||||
fn process_enable_enterprise_attestation(
|
||||
env: &mut impl Env,
|
||||
) -> Result<ResponseData, Ctap2StatusCode> {
|
||||
if env.customization().enterprise_attestation_mode().is_some() {
|
||||
storage::enable_enterprise_attestation(env)?;
|
||||
Ok(ResponseData::AuthenticatorConfig)
|
||||
} else {
|
||||
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes the subcommand toggleAlwaysUv for AuthenticatorConfig.
|
||||
fn process_toggle_always_uv(env: &mut impl Env) -> Result<ResponseData, Ctap2StatusCode> {
|
||||
storage::toggle_always_uv(env)?;
|
||||
Ok(ResponseData::AuthenticatorConfig)
|
||||
}
|
||||
|
||||
/// Processes the subcommand setMinPINLength for AuthenticatorConfig.
|
||||
fn process_set_min_pin_length(
|
||||
env: &mut impl Env,
|
||||
params: SetMinPinLengthParams,
|
||||
) -> Result<ResponseData, Ctap2StatusCode> {
|
||||
let SetMinPinLengthParams {
|
||||
new_min_pin_length,
|
||||
min_pin_length_rp_ids,
|
||||
force_change_pin,
|
||||
} = params;
|
||||
let store_min_pin_length = storage::min_pin_length(env)?;
|
||||
let new_min_pin_length = new_min_pin_length.unwrap_or(store_min_pin_length);
|
||||
if new_min_pin_length < store_min_pin_length {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION);
|
||||
}
|
||||
let mut force_change_pin = force_change_pin.unwrap_or(false);
|
||||
if force_change_pin && storage::pin_hash(env)?.is_none() {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET);
|
||||
}
|
||||
if let Some(old_length) = storage::pin_code_point_length(env)? {
|
||||
force_change_pin |= new_min_pin_length > old_length;
|
||||
}
|
||||
if force_change_pin {
|
||||
storage::force_pin_change(env)?;
|
||||
}
|
||||
storage::set_min_pin_length(env, new_min_pin_length)?;
|
||||
if let Some(min_pin_length_rp_ids) = min_pin_length_rp_ids {
|
||||
storage::set_min_pin_length_rp_ids(env, min_pin_length_rp_ids)?;
|
||||
}
|
||||
Ok(ResponseData::AuthenticatorConfig)
|
||||
}
|
||||
|
||||
/// Processes the AuthenticatorConfig command.
|
||||
pub fn process_config<E: Env>(
|
||||
env: &mut E,
|
||||
client_pin: &mut ClientPin<E>,
|
||||
params: AuthenticatorConfigParameters,
|
||||
) -> Result<ResponseData, Ctap2StatusCode> {
|
||||
let AuthenticatorConfigParameters {
|
||||
sub_command,
|
||||
sub_command_params,
|
||||
pin_uv_auth_protocol,
|
||||
pin_uv_auth_param,
|
||||
} = params;
|
||||
|
||||
let enforce_uv =
|
||||
!matches!(sub_command, ConfigSubCommand::ToggleAlwaysUv) && storage::has_always_uv(env)?;
|
||||
if storage::pin_hash(env)?.is_some() || enforce_uv {
|
||||
let pin_uv_auth_param =
|
||||
pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?;
|
||||
let pin_uv_auth_protocol =
|
||||
pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?;
|
||||
// Constants are taken from the specification, section 6.11, step 4.2.
|
||||
let mut config_data = vec![0xFF; 32];
|
||||
config_data.extend(&[0x0D, sub_command as u8]);
|
||||
if let Some(sub_command_params) = sub_command_params.clone() {
|
||||
super::cbor_write(sub_command_params.into(), &mut config_data)?;
|
||||
}
|
||||
client_pin.verify_pin_uv_auth_token(
|
||||
&config_data,
|
||||
&pin_uv_auth_param,
|
||||
pin_uv_auth_protocol,
|
||||
)?;
|
||||
client_pin.has_permission(PinPermission::AuthenticatorConfiguration)?;
|
||||
}
|
||||
|
||||
match sub_command {
|
||||
ConfigSubCommand::EnableEnterpriseAttestation => process_enable_enterprise_attestation(env),
|
||||
ConfigSubCommand::ToggleAlwaysUv => process_toggle_always_uv(env),
|
||||
ConfigSubCommand::SetMinPinLength => {
|
||||
if let Some(ConfigSubCommandParams::SetMinPinLength(params)) = sub_command_params {
|
||||
process_set_min_pin_length(env, params)
|
||||
} else {
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)
|
||||
}
|
||||
}
|
||||
_ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::api::customization::Customization;
|
||||
use crate::ctap::data_formats::PinUvAuthProtocol;
|
||||
use crate::ctap::pin_protocol::authenticate_pin_uv_auth_token;
|
||||
use crate::env::test::TestEnv;
|
||||
|
||||
#[test]
|
||||
fn test_process_enable_enterprise_attestation() {
|
||||
let mut env = TestEnv::default();
|
||||
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
|
||||
let pin_uv_auth_token = [0x55; 32];
|
||||
let mut client_pin = ClientPin::<TestEnv>::new_test(
|
||||
&mut env,
|
||||
key_agreement_key,
|
||||
pin_uv_auth_token,
|
||||
PinUvAuthProtocol::V1,
|
||||
);
|
||||
|
||||
let config_params = AuthenticatorConfigParameters {
|
||||
sub_command: ConfigSubCommand::EnableEnterpriseAttestation,
|
||||
sub_command_params: None,
|
||||
pin_uv_auth_param: None,
|
||||
pin_uv_auth_protocol: None,
|
||||
};
|
||||
let config_response = process_config(&mut env, &mut client_pin, config_params);
|
||||
|
||||
if env.customization().enterprise_attestation_mode().is_some() {
|
||||
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
|
||||
assert_eq!(storage::enterprise_attestation(&mut env), Ok(true));
|
||||
} else {
|
||||
assert_eq!(
|
||||
config_response,
|
||||
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_toggle_always_uv() {
|
||||
let mut env = TestEnv::default();
|
||||
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
|
||||
let pin_uv_auth_token = [0x55; 32];
|
||||
let mut client_pin = ClientPin::<TestEnv>::new_test(
|
||||
&mut env,
|
||||
key_agreement_key,
|
||||
pin_uv_auth_token,
|
||||
PinUvAuthProtocol::V1,
|
||||
);
|
||||
|
||||
let config_params = AuthenticatorConfigParameters {
|
||||
sub_command: ConfigSubCommand::ToggleAlwaysUv,
|
||||
sub_command_params: None,
|
||||
pin_uv_auth_param: None,
|
||||
pin_uv_auth_protocol: None,
|
||||
};
|
||||
let config_response = process_config(&mut env, &mut client_pin, config_params);
|
||||
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
|
||||
assert!(storage::has_always_uv(&mut env).unwrap());
|
||||
|
||||
let config_params = AuthenticatorConfigParameters {
|
||||
sub_command: ConfigSubCommand::ToggleAlwaysUv,
|
||||
sub_command_params: None,
|
||||
pin_uv_auth_param: None,
|
||||
pin_uv_auth_protocol: None,
|
||||
};
|
||||
let config_response = process_config(&mut env, &mut client_pin, config_params);
|
||||
if env.customization().enforce_always_uv() {
|
||||
assert_eq!(
|
||||
config_response,
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED)
|
||||
);
|
||||
} else {
|
||||
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
|
||||
assert!(!storage::has_always_uv(&mut env).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
fn test_helper_process_toggle_always_uv_with_pin(pin_uv_auth_protocol: PinUvAuthProtocol) {
|
||||
let mut env = TestEnv::default();
|
||||
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
|
||||
let pin_uv_auth_token = [0x55; 32];
|
||||
let mut client_pin = ClientPin::<TestEnv>::new_test(
|
||||
&mut env,
|
||||
key_agreement_key,
|
||||
pin_uv_auth_token,
|
||||
pin_uv_auth_protocol,
|
||||
);
|
||||
storage::set_pin(&mut env, &[0x88; 16], 4).unwrap();
|
||||
|
||||
let mut config_data = vec![0xFF; 32];
|
||||
config_data.extend(&[0x0D, ConfigSubCommand::ToggleAlwaysUv as u8]);
|
||||
let pin_uv_auth_param =
|
||||
authenticate_pin_uv_auth_token(&pin_uv_auth_token, &config_data, pin_uv_auth_protocol);
|
||||
let config_params = AuthenticatorConfigParameters {
|
||||
sub_command: ConfigSubCommand::ToggleAlwaysUv,
|
||||
sub_command_params: None,
|
||||
pin_uv_auth_param: Some(pin_uv_auth_param.clone()),
|
||||
pin_uv_auth_protocol: Some(pin_uv_auth_protocol),
|
||||
};
|
||||
let config_response = process_config(&mut env, &mut client_pin, config_params);
|
||||
if env.customization().enforce_always_uv() {
|
||||
assert_eq!(
|
||||
config_response,
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED)
|
||||
);
|
||||
return;
|
||||
}
|
||||
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
|
||||
assert!(storage::has_always_uv(&mut env).unwrap());
|
||||
|
||||
let config_params = AuthenticatorConfigParameters {
|
||||
sub_command: ConfigSubCommand::ToggleAlwaysUv,
|
||||
sub_command_params: None,
|
||||
pin_uv_auth_param: Some(pin_uv_auth_param),
|
||||
pin_uv_auth_protocol: Some(pin_uv_auth_protocol),
|
||||
};
|
||||
let config_response = process_config(&mut env, &mut client_pin, config_params);
|
||||
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
|
||||
assert!(!storage::has_always_uv(&mut env).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_toggle_always_uv_with_pin_v1() {
|
||||
test_helper_process_toggle_always_uv_with_pin(PinUvAuthProtocol::V1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_toggle_always_uv_with_pin_v2() {
|
||||
test_helper_process_toggle_always_uv_with_pin(PinUvAuthProtocol::V2);
|
||||
}
|
||||
|
||||
fn create_min_pin_config_params(
|
||||
min_pin_length: u8,
|
||||
min_pin_length_rp_ids: Option<Vec<String>>,
|
||||
) -> AuthenticatorConfigParameters {
|
||||
let set_min_pin_length_params = SetMinPinLengthParams {
|
||||
new_min_pin_length: Some(min_pin_length),
|
||||
min_pin_length_rp_ids,
|
||||
force_change_pin: None,
|
||||
};
|
||||
AuthenticatorConfigParameters {
|
||||
sub_command: ConfigSubCommand::SetMinPinLength,
|
||||
sub_command_params: Some(ConfigSubCommandParams::SetMinPinLength(
|
||||
set_min_pin_length_params,
|
||||
)),
|
||||
pin_uv_auth_param: None,
|
||||
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_set_min_pin_length() {
|
||||
let mut env = TestEnv::default();
|
||||
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
|
||||
let pin_uv_auth_token = [0x55; 32];
|
||||
let mut client_pin = ClientPin::<TestEnv>::new_test(
|
||||
&mut env,
|
||||
key_agreement_key,
|
||||
pin_uv_auth_token,
|
||||
PinUvAuthProtocol::V1,
|
||||
);
|
||||
|
||||
// First, increase minimum PIN length from 4 to 6 without PIN auth.
|
||||
let min_pin_length = 6;
|
||||
let config_params = create_min_pin_config_params(min_pin_length, None);
|
||||
let config_response = process_config(&mut env, &mut client_pin, config_params);
|
||||
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
|
||||
assert_eq!(storage::min_pin_length(&mut env), Ok(min_pin_length));
|
||||
|
||||
// Second, increase minimum PIN length from 6 to 8 with PIN auth.
|
||||
// The stored PIN or its length don't matter since we control the token.
|
||||
storage::set_pin(&mut env, &[0x88; 16], 8).unwrap();
|
||||
let min_pin_length = 8;
|
||||
let mut config_params = create_min_pin_config_params(min_pin_length, None);
|
||||
let pin_uv_auth_param = vec![
|
||||
0x5C, 0x69, 0x71, 0x29, 0xBD, 0xCC, 0x53, 0xE8, 0x3C, 0x97, 0x62, 0xDD, 0x90, 0x29,
|
||||
0xB2, 0xDE,
|
||||
];
|
||||
config_params.pin_uv_auth_param = Some(pin_uv_auth_param);
|
||||
let config_response = process_config(&mut env, &mut client_pin, config_params);
|
||||
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
|
||||
assert_eq!(storage::min_pin_length(&mut env), Ok(min_pin_length));
|
||||
|
||||
// Third, decreasing the minimum PIN length from 8 to 7 fails.
|
||||
let mut config_params = create_min_pin_config_params(7, None);
|
||||
let pin_uv_auth_param = vec![
|
||||
0xC5, 0xEA, 0xC1, 0x5E, 0x7F, 0x80, 0x70, 0x1A, 0x4E, 0xC4, 0xAD, 0x85, 0x35, 0xD8,
|
||||
0xA7, 0x71,
|
||||
];
|
||||
config_params.pin_uv_auth_param = Some(pin_uv_auth_param);
|
||||
let config_response = process_config(&mut env, &mut client_pin, config_params);
|
||||
assert_eq!(
|
||||
config_response,
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION)
|
||||
);
|
||||
assert_eq!(storage::min_pin_length(&mut env), Ok(min_pin_length));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_set_min_pin_length_rp_ids() {
|
||||
let mut env = TestEnv::default();
|
||||
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
|
||||
let pin_uv_auth_token = [0x55; 32];
|
||||
let mut client_pin = ClientPin::<TestEnv>::new_test(
|
||||
&mut env,
|
||||
key_agreement_key,
|
||||
pin_uv_auth_token,
|
||||
PinUvAuthProtocol::V1,
|
||||
);
|
||||
|
||||
// First, set RP IDs without PIN auth.
|
||||
let min_pin_length = 6;
|
||||
let min_pin_length_rp_ids = vec!["example.com".to_string()];
|
||||
let config_params =
|
||||
create_min_pin_config_params(min_pin_length, Some(min_pin_length_rp_ids.clone()));
|
||||
let config_response = process_config(&mut env, &mut client_pin, config_params);
|
||||
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
|
||||
assert_eq!(storage::min_pin_length(&mut env), Ok(min_pin_length));
|
||||
assert_eq!(
|
||||
storage::min_pin_length_rp_ids(&mut env),
|
||||
Ok(min_pin_length_rp_ids)
|
||||
);
|
||||
|
||||
// Second, change the RP IDs with PIN auth.
|
||||
let min_pin_length = 8;
|
||||
let min_pin_length_rp_ids = vec!["another.example.com".to_string()];
|
||||
// The stored PIN or its length don't matter since we control the token.
|
||||
storage::set_pin(&mut env, &[0x88; 16], 8).unwrap();
|
||||
let mut config_params =
|
||||
create_min_pin_config_params(min_pin_length, Some(min_pin_length_rp_ids.clone()));
|
||||
let pin_uv_auth_param = vec![
|
||||
0x40, 0x51, 0x2D, 0xAC, 0x2D, 0xE2, 0x15, 0x77, 0x5C, 0xF9, 0x5B, 0x62, 0x9A, 0x2D,
|
||||
0xD6, 0xDA,
|
||||
];
|
||||
config_params.pin_uv_auth_param = Some(pin_uv_auth_param.clone());
|
||||
let config_response = process_config(&mut env, &mut client_pin, config_params);
|
||||
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
|
||||
assert_eq!(storage::min_pin_length(&mut env), Ok(min_pin_length));
|
||||
assert_eq!(
|
||||
storage::min_pin_length_rp_ids(&mut env),
|
||||
Ok(min_pin_length_rp_ids.clone())
|
||||
);
|
||||
|
||||
// Third, changing RP IDs with bad PIN auth fails.
|
||||
// One PIN auth shouldn't work for different lengths.
|
||||
let mut config_params =
|
||||
create_min_pin_config_params(9, Some(min_pin_length_rp_ids.clone()));
|
||||
config_params.pin_uv_auth_param = Some(pin_uv_auth_param.clone());
|
||||
let config_response = process_config(&mut env, &mut client_pin, config_params);
|
||||
assert_eq!(
|
||||
config_response,
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
|
||||
);
|
||||
assert_eq!(storage::min_pin_length(&mut env), Ok(min_pin_length));
|
||||
assert_eq!(
|
||||
storage::min_pin_length_rp_ids(&mut env),
|
||||
Ok(min_pin_length_rp_ids.clone())
|
||||
);
|
||||
|
||||
// Forth, changing RP IDs with bad PIN auth fails.
|
||||
// One PIN auth shouldn't work for different RP IDs.
|
||||
let mut config_params = create_min_pin_config_params(
|
||||
min_pin_length,
|
||||
Some(vec!["counter.example.com".to_string()]),
|
||||
);
|
||||
config_params.pin_uv_auth_param = Some(pin_uv_auth_param);
|
||||
let config_response = process_config(&mut env, &mut client_pin, config_params);
|
||||
assert_eq!(
|
||||
config_response,
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
|
||||
);
|
||||
assert_eq!(storage::min_pin_length(&mut env), Ok(min_pin_length));
|
||||
assert_eq!(
|
||||
storage::min_pin_length_rp_ids(&mut env),
|
||||
Ok(min_pin_length_rp_ids)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_set_min_pin_length_force_pin_change_implicit() {
|
||||
let mut env = TestEnv::default();
|
||||
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
|
||||
let pin_uv_auth_token = [0x55; 32];
|
||||
let mut client_pin = ClientPin::<TestEnv>::new_test(
|
||||
&mut env,
|
||||
key_agreement_key,
|
||||
pin_uv_auth_token,
|
||||
PinUvAuthProtocol::V1,
|
||||
);
|
||||
|
||||
storage::set_pin(&mut env, &[0x88; 16], 4).unwrap();
|
||||
// Increase min PIN, force PIN change.
|
||||
let min_pin_length = 6;
|
||||
let mut config_params = create_min_pin_config_params(min_pin_length, None);
|
||||
let pin_uv_auth_param = Some(vec![
|
||||
0x81, 0x37, 0x37, 0xF3, 0xD8, 0x69, 0xBD, 0x74, 0xFE, 0x88, 0x30, 0x8C, 0xC4, 0x2E,
|
||||
0xA8, 0xC8,
|
||||
]);
|
||||
config_params.pin_uv_auth_param = pin_uv_auth_param;
|
||||
let config_response = process_config(&mut env, &mut client_pin, config_params);
|
||||
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
|
||||
assert_eq!(storage::min_pin_length(&mut env), Ok(min_pin_length));
|
||||
assert_eq!(storage::has_force_pin_change(&mut env), Ok(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_set_min_pin_length_force_pin_change_explicit() {
|
||||
let mut env = TestEnv::default();
|
||||
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
|
||||
let pin_uv_auth_token = [0x55; 32];
|
||||
let mut client_pin = ClientPin::<TestEnv>::new_test(
|
||||
&mut env,
|
||||
key_agreement_key,
|
||||
pin_uv_auth_token,
|
||||
PinUvAuthProtocol::V1,
|
||||
);
|
||||
|
||||
storage::set_pin(&mut env, &[0x88; 16], 4).unwrap();
|
||||
let pin_uv_auth_param = Some(vec![
|
||||
0xE3, 0x74, 0xF4, 0x27, 0xBE, 0x7D, 0x40, 0xB5, 0x71, 0xB6, 0xB4, 0x1A, 0xD2, 0xC1,
|
||||
0x53, 0xD7,
|
||||
]);
|
||||
let set_min_pin_length_params = SetMinPinLengthParams {
|
||||
new_min_pin_length: Some(storage::min_pin_length(&mut env).unwrap()),
|
||||
min_pin_length_rp_ids: None,
|
||||
force_change_pin: Some(true),
|
||||
};
|
||||
let config_params = AuthenticatorConfigParameters {
|
||||
sub_command: ConfigSubCommand::SetMinPinLength,
|
||||
sub_command_params: Some(ConfigSubCommandParams::SetMinPinLength(
|
||||
set_min_pin_length_params,
|
||||
)),
|
||||
pin_uv_auth_param,
|
||||
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
|
||||
};
|
||||
let config_response = process_config(&mut env, &mut client_pin, config_params);
|
||||
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
|
||||
assert_eq!(storage::has_force_pin_change(&mut env), Ok(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_config_vendor_prototype() {
|
||||
let mut env = TestEnv::default();
|
||||
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
|
||||
let pin_uv_auth_token = [0x55; 32];
|
||||
let mut client_pin = ClientPin::<TestEnv>::new_test(
|
||||
&mut env,
|
||||
key_agreement_key,
|
||||
pin_uv_auth_token,
|
||||
PinUvAuthProtocol::V1,
|
||||
);
|
||||
|
||||
let config_params = AuthenticatorConfigParameters {
|
||||
sub_command: ConfigSubCommand::VendorPrototype,
|
||||
sub_command_params: None,
|
||||
pin_uv_auth_param: None,
|
||||
pin_uv_auth_protocol: None,
|
||||
};
|
||||
let config_response = process_config(&mut env, &mut client_pin, config_params);
|
||||
assert_eq!(
|
||||
config_response,
|
||||
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
|
||||
);
|
||||
}
|
||||
}
|
||||
494
libraries/opensk/src/ctap/credential_id.rs
Normal file
494
libraries/opensk/src/ctap/credential_id.rs
Normal file
@@ -0,0 +1,494 @@
|
||||
// Copyright 2022-2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::crypto_wrapper::{aes256_cbc_decrypt, aes256_cbc_encrypt, PrivateKey};
|
||||
use super::data_formats::{
|
||||
CredentialProtectionPolicy, PublicKeyCredentialSource, PublicKeyCredentialType,
|
||||
};
|
||||
use super::status_code::Ctap2StatusCode;
|
||||
use super::{cbor_read, cbor_write};
|
||||
use crate::api::key_store::KeyStore;
|
||||
use crate::ctap::data_formats::{extract_byte_string, extract_map};
|
||||
use crate::env::Env;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use core::convert::{TryFrom, TryInto};
|
||||
use crypto::hmac::{hmac_256, verify_hmac_256};
|
||||
use crypto::sha256::Sha256;
|
||||
use sk_cbor::{cbor_map_options, destructure_cbor_map};
|
||||
|
||||
pub const LEGACY_CREDENTIAL_ID_SIZE: usize = 112;
|
||||
// CBOR credential IDs consist of
|
||||
// - 1 byte : version number
|
||||
// - 16 bytes: initialization vector for AES-256,
|
||||
// - 192 bytes: encrypted block of the key handle cbor,
|
||||
// - 32 bytes: HMAC-SHA256 over everything else.
|
||||
pub const CBOR_CREDENTIAL_ID_SIZE: usize = 241;
|
||||
pub const MIN_CREDENTIAL_ID_SIZE: usize = LEGACY_CREDENTIAL_ID_SIZE;
|
||||
pub const MAX_CREDENTIAL_ID_SIZE: usize = CBOR_CREDENTIAL_ID_SIZE;
|
||||
|
||||
pub const CBOR_CREDENTIAL_ID_VERSION: u8 = 0x01;
|
||||
|
||||
pub const MAX_PADDING_LENGTH: u8 = 0xBF;
|
||||
|
||||
// Data fields that are contained in the credential ID of non-discoverable credentials.
|
||||
struct CredentialSource {
|
||||
private_key: PrivateKey,
|
||||
rp_id_hash: [u8; 32],
|
||||
cred_protect_policy: Option<CredentialProtectionPolicy>,
|
||||
cred_blob: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
// The data fields contained in the credential ID are serialized using CBOR maps.
|
||||
// Each field is associated with a unique tag, implemented with a CBOR unsigned key.
|
||||
enum CredentialSourceField {
|
||||
PrivateKey = 0,
|
||||
RpIdHash = 1,
|
||||
CredProtectPolicy = 2,
|
||||
CredBlob = 3,
|
||||
}
|
||||
|
||||
impl From<CredentialSourceField> for sk_cbor::Value {
|
||||
fn from(field: CredentialSourceField) -> sk_cbor::Value {
|
||||
(field as u64).into()
|
||||
}
|
||||
}
|
||||
|
||||
fn decrypt_legacy_credential_id(
|
||||
env: &mut impl Env,
|
||||
bytes: &[u8],
|
||||
) -> Result<Option<CredentialSource>, Ctap2StatusCode> {
|
||||
let aes_enc_key = crypto::aes256::EncryptionKey::new(&env.key_store().key_handle_encryption()?);
|
||||
let plaintext = aes256_cbc_decrypt(&aes_enc_key, bytes, true)?;
|
||||
if plaintext.len() != 64 {
|
||||
return Ok(None);
|
||||
}
|
||||
let private_key = if let Some(key) = PrivateKey::new_ecdsa_from_bytes(&plaintext[..32]) {
|
||||
key
|
||||
} else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(Some(CredentialSource {
|
||||
private_key,
|
||||
rp_id_hash: plaintext[32..64].try_into().unwrap(),
|
||||
cred_protect_policy: None,
|
||||
cred_blob: None,
|
||||
}))
|
||||
}
|
||||
|
||||
fn decrypt_cbor_credential_id(
|
||||
env: &mut impl Env,
|
||||
bytes: &[u8],
|
||||
) -> Result<Option<CredentialSource>, Ctap2StatusCode> {
|
||||
let aes_enc_key = crypto::aes256::EncryptionKey::new(&env.key_store().key_handle_encryption()?);
|
||||
let mut plaintext = aes256_cbc_decrypt(&aes_enc_key, bytes, true)?;
|
||||
remove_padding(&mut plaintext)?;
|
||||
|
||||
let cbor_credential_source = cbor_read(plaintext.as_slice())?;
|
||||
destructure_cbor_map! {
|
||||
let {
|
||||
CredentialSourceField::PrivateKey => private_key,
|
||||
CredentialSourceField::RpIdHash=> rp_id_hash,
|
||||
CredentialSourceField::CredProtectPolicy => cred_protect_policy,
|
||||
CredentialSourceField::CredBlob => cred_blob,
|
||||
} = extract_map(cbor_credential_source)?;
|
||||
}
|
||||
Ok(match (private_key, rp_id_hash) {
|
||||
(Some(private_key), Some(rp_id_hash)) => {
|
||||
let private_key = PrivateKey::try_from(private_key)?;
|
||||
let rp_id_hash = extract_byte_string(rp_id_hash)?;
|
||||
if rp_id_hash.len() != 32 {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
|
||||
}
|
||||
let cred_protect_policy = cred_protect_policy
|
||||
.map(CredentialProtectionPolicy::try_from)
|
||||
.transpose()?;
|
||||
let cred_blob = cred_blob.map(extract_byte_string).transpose()?;
|
||||
Some(CredentialSource {
|
||||
private_key,
|
||||
rp_id_hash: rp_id_hash.try_into().unwrap(),
|
||||
cred_protect_policy,
|
||||
cred_blob,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Pad data to MAX_PADDING_LENGTH+1 (192) bytes using PKCS padding scheme.
|
||||
/// Let N = 192 - data.len(), the PKCS padding scheme would pad N bytes of N after the data.
|
||||
fn add_padding(data: &mut Vec<u8>) -> Result<(), Ctap2StatusCode> {
|
||||
// The data should be between 1 to MAX_PADDING_LENGTH bytes for the padding scheme to be valid.
|
||||
if data.is_empty() || data.len() > MAX_PADDING_LENGTH as usize {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
|
||||
}
|
||||
let pad_length = MAX_PADDING_LENGTH - (data.len() as u8 - 1);
|
||||
data.extend(core::iter::repeat(pad_length).take(pad_length as usize));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_padding(data: &mut Vec<u8>) -> Result<(), Ctap2StatusCode> {
|
||||
if data.len() != MAX_PADDING_LENGTH as usize + 1 {
|
||||
// This is an internal error instead of corrupted credential ID which we should just ignore because
|
||||
// we've already checked that the HMAC matched.
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
|
||||
}
|
||||
let pad_length = *data.last().unwrap();
|
||||
if pad_length == 0 || pad_length > MAX_PADDING_LENGTH {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
|
||||
}
|
||||
if !data
|
||||
.drain((data.len() - pad_length as usize)..)
|
||||
.all(|x| x == pad_length)
|
||||
{
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Encrypts the given private key, relying party ID hash, and some other metadata into a credential ID.
|
||||
///
|
||||
/// Other information, such as a user name, are not stored. Since encrypted credential IDs are
|
||||
/// stored server-side, this information is already available (unencrypted).
|
||||
pub fn encrypt_to_credential_id(
|
||||
env: &mut impl Env,
|
||||
private_key: &PrivateKey,
|
||||
rp_id_hash: &[u8; 32],
|
||||
cred_protect_policy: Option<CredentialProtectionPolicy>,
|
||||
cred_blob: Option<Vec<u8>>,
|
||||
) -> Result<Vec<u8>, Ctap2StatusCode> {
|
||||
let mut payload = Vec::new();
|
||||
let cbor = cbor_map_options! {
|
||||
CredentialSourceField::PrivateKey => private_key,
|
||||
CredentialSourceField::RpIdHash=> rp_id_hash,
|
||||
CredentialSourceField::CredProtectPolicy => cred_protect_policy,
|
||||
CredentialSourceField::CredBlob => cred_blob,
|
||||
};
|
||||
cbor_write(cbor, &mut payload)?;
|
||||
add_padding(&mut payload)?;
|
||||
|
||||
let aes_enc_key = crypto::aes256::EncryptionKey::new(&env.key_store().key_handle_encryption()?);
|
||||
let encrypted_payload = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &payload, true)?;
|
||||
let mut credential_id = encrypted_payload;
|
||||
credential_id.insert(0, CBOR_CREDENTIAL_ID_VERSION);
|
||||
|
||||
let id_hmac = hmac_256::<Sha256>(
|
||||
&env.key_store().key_handle_authentication()?,
|
||||
&credential_id[..],
|
||||
);
|
||||
credential_id.extend(&id_hmac);
|
||||
Ok(credential_id)
|
||||
}
|
||||
|
||||
/// Decrypts the given credential ID into a PublicKeyCredentialSource, populating only the recorded fields.
|
||||
///
|
||||
/// Returns None if
|
||||
/// - the format does not match any known versions, or
|
||||
/// - the HMAC test fails.
|
||||
///
|
||||
/// For v0 (legacy U2F) the credential ID consists of:
|
||||
/// - 16 bytes: initialization vector for AES-256,
|
||||
/// - 32 bytes: encrypted ECDSA private key for the credential,
|
||||
/// - 32 bytes: encrypted relying party ID hashed with SHA256,
|
||||
/// - 32 bytes: HMAC-SHA256 over everything else.
|
||||
///
|
||||
/// For v1 (CBOR) the credential ID consists of:
|
||||
/// - 1 byte : version number,
|
||||
/// - 16 bytes: initialization vector for AES-256,
|
||||
/// - 192 bytes: encrypted CBOR-encoded credential source fields,
|
||||
/// - 32 bytes: HMAC-SHA256 over everything else.
|
||||
pub fn decrypt_credential_id(
|
||||
env: &mut impl Env,
|
||||
credential_id: Vec<u8>,
|
||||
rp_id_hash: &[u8],
|
||||
) -> Result<Option<PublicKeyCredentialSource>, Ctap2StatusCode> {
|
||||
if credential_id.len() < MIN_CREDENTIAL_ID_SIZE {
|
||||
return Ok(None);
|
||||
}
|
||||
let hmac_message_size = credential_id.len() - 32;
|
||||
if !verify_hmac_256::<Sha256>(
|
||||
&env.key_store().key_handle_authentication()?,
|
||||
&credential_id[..hmac_message_size],
|
||||
array_ref![credential_id, hmac_message_size, 32],
|
||||
) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let credential_source = if credential_id.len() == LEGACY_CREDENTIAL_ID_SIZE {
|
||||
decrypt_legacy_credential_id(env, &credential_id[..hmac_message_size])?
|
||||
} else {
|
||||
match credential_id[0] {
|
||||
CBOR_CREDENTIAL_ID_VERSION => {
|
||||
if credential_id.len() != CBOR_CREDENTIAL_ID_SIZE {
|
||||
return Ok(None);
|
||||
}
|
||||
decrypt_cbor_credential_id(env, &credential_id[1..hmac_message_size])?
|
||||
}
|
||||
_ => return Ok(None),
|
||||
}
|
||||
};
|
||||
|
||||
let credential_source = if let Some(credential_source) = credential_source {
|
||||
credential_source
|
||||
} else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if rp_id_hash != credential_source.rp_id_hash {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(PublicKeyCredentialSource {
|
||||
key_type: PublicKeyCredentialType::PublicKey,
|
||||
credential_id,
|
||||
private_key: credential_source.private_key,
|
||||
rp_id: String::new(),
|
||||
user_handle: Vec::new(),
|
||||
user_display_name: None,
|
||||
cred_protect_policy: credential_source.cred_protect_policy,
|
||||
creation_order: 0,
|
||||
user_name: None,
|
||||
user_icon: None,
|
||||
cred_blob: credential_source.cred_blob,
|
||||
large_blob_key: None,
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::api::customization::Customization;
|
||||
use crate::ctap::credential_id::CBOR_CREDENTIAL_ID_SIZE;
|
||||
use crate::ctap::SignatureAlgorithm;
|
||||
use crate::env::test::TestEnv;
|
||||
use crypto::hmac::hmac_256;
|
||||
|
||||
const UNSUPPORTED_CREDENTIAL_ID_VERSION: u8 = 0x80;
|
||||
|
||||
fn test_encrypt_decrypt_credential(signature_algorithm: SignatureAlgorithm) {
|
||||
let mut env = TestEnv::default();
|
||||
let private_key = PrivateKey::new(&mut env, signature_algorithm);
|
||||
|
||||
let rp_id_hash = [0x55; 32];
|
||||
let encrypted_id =
|
||||
encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, None).unwrap();
|
||||
let decrypted_source = decrypt_credential_id(&mut env, encrypted_id, &rp_id_hash)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(private_key, decrypted_source.private_key);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_decrypt_ecdsa_credential() {
|
||||
test_encrypt_decrypt_credential(SignatureAlgorithm::Es256);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "ed25519")]
|
||||
fn test_encrypt_decrypt_ed25519_credential() {
|
||||
test_encrypt_decrypt_credential(SignatureAlgorithm::Eddsa);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_decrypt_bad_version() {
|
||||
let mut env = TestEnv::default();
|
||||
let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
|
||||
|
||||
let rp_id_hash = [0x55; 32];
|
||||
let mut encrypted_id =
|
||||
encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, None).unwrap();
|
||||
encrypted_id[0] = UNSUPPORTED_CREDENTIAL_ID_VERSION;
|
||||
// Override the HMAC to pass the check.
|
||||
encrypted_id.truncate(&encrypted_id.len() - 32);
|
||||
let hmac_key = env.key_store().key_handle_authentication().unwrap();
|
||||
let id_hmac = hmac_256::<Sha256>(&hmac_key, &encrypted_id[..]);
|
||||
encrypted_id.extend(&id_hmac);
|
||||
|
||||
assert_eq!(
|
||||
decrypt_credential_id(&mut env, encrypted_id, &rp_id_hash),
|
||||
Ok(None)
|
||||
);
|
||||
}
|
||||
|
||||
fn test_encrypt_decrypt_bad_hmac(signature_algorithm: SignatureAlgorithm) {
|
||||
let mut env = TestEnv::default();
|
||||
let private_key = PrivateKey::new(&mut env, signature_algorithm);
|
||||
|
||||
let rp_id_hash = [0x55; 32];
|
||||
let encrypted_id =
|
||||
encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, None).unwrap();
|
||||
for i in 0..encrypted_id.len() {
|
||||
let mut modified_id = encrypted_id.clone();
|
||||
modified_id[i] ^= 0x01;
|
||||
assert_eq!(
|
||||
decrypt_credential_id(&mut env, modified_id, &rp_id_hash),
|
||||
Ok(None)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ecdsa_encrypt_decrypt_bad_hmac() {
|
||||
test_encrypt_decrypt_bad_hmac(SignatureAlgorithm::Es256);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "ed25519")]
|
||||
fn test_ed25519_encrypt_decrypt_bad_hmac() {
|
||||
test_encrypt_decrypt_bad_hmac(SignatureAlgorithm::Eddsa);
|
||||
}
|
||||
|
||||
fn test_decrypt_credential_missing_blocks(signature_algorithm: SignatureAlgorithm) {
|
||||
let mut env = TestEnv::default();
|
||||
let private_key = PrivateKey::new(&mut env, signature_algorithm);
|
||||
|
||||
let rp_id_hash = [0x55; 32];
|
||||
let encrypted_id =
|
||||
encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, None).unwrap();
|
||||
|
||||
for length in (1..CBOR_CREDENTIAL_ID_SIZE).step_by(16) {
|
||||
assert_eq!(
|
||||
decrypt_credential_id(&mut env, encrypted_id[..length].to_vec(), &rp_id_hash),
|
||||
Ok(None)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ecdsa_decrypt_credential_missing_blocks() {
|
||||
test_decrypt_credential_missing_blocks(SignatureAlgorithm::Es256);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "ed25519")]
|
||||
fn test_ed25519_decrypt_credential_missing_blocks() {
|
||||
test_decrypt_credential_missing_blocks(SignatureAlgorithm::Eddsa);
|
||||
}
|
||||
|
||||
/// This is a copy of the function that genereated deprecated key handles.
|
||||
fn legacy_encrypt_to_credential_id(
|
||||
env: &mut impl Env,
|
||||
private_key: crypto::ecdsa::SecKey,
|
||||
application: &[u8; 32],
|
||||
) -> Result<Vec<u8>, Ctap2StatusCode> {
|
||||
let aes_enc_key =
|
||||
crypto::aes256::EncryptionKey::new(&env.key_store().key_handle_encryption()?);
|
||||
let mut plaintext = [0; 64];
|
||||
private_key.to_bytes(array_mut_ref!(plaintext, 0, 32));
|
||||
plaintext[32..64].copy_from_slice(application);
|
||||
|
||||
let mut encrypted_id = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, true)?;
|
||||
let id_hmac = hmac_256::<Sha256>(
|
||||
&env.key_store().key_handle_authentication()?,
|
||||
&encrypted_id[..],
|
||||
);
|
||||
encrypted_id.extend(&id_hmac);
|
||||
Ok(encrypted_id)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_decrypt_credential_legacy() {
|
||||
let mut env = TestEnv::default();
|
||||
let private_key = PrivateKey::new_ecdsa(&mut env);
|
||||
let ecdsa_key = private_key.ecdsa_key(&mut env).unwrap();
|
||||
|
||||
let rp_id_hash = [0x55; 32];
|
||||
let encrypted_id =
|
||||
legacy_encrypt_to_credential_id(&mut env, ecdsa_key, &rp_id_hash).unwrap();
|
||||
let decrypted_source = decrypt_credential_id(&mut env, encrypted_id, &rp_id_hash)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(private_key, decrypted_source.private_key);
|
||||
// Legacy credentials didn't persist credProtectPolicy info, so it should be treated as None.
|
||||
assert!(decrypted_source.cred_protect_policy.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_credential_size() {
|
||||
let mut env = TestEnv::default();
|
||||
let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
|
||||
|
||||
let rp_id_hash = [0x55; 32];
|
||||
let encrypted_id =
|
||||
encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, None).unwrap();
|
||||
assert_eq!(encrypted_id.len(), CBOR_CREDENTIAL_ID_SIZE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_credential_max_cbor_size() {
|
||||
// The cbor encoding length is variadic and depends on size of fields. Try to put maximum length
|
||||
// for each encoded field and ensure that it doesn't go over the padding size.
|
||||
let mut env = TestEnv::default();
|
||||
// Currently all private key types have same length when transformed to bytes.
|
||||
let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
|
||||
let rp_id_hash = [0x55; 32];
|
||||
let cred_protect_policy = Some(CredentialProtectionPolicy::UserVerificationOptional);
|
||||
let cred_blob = Some(vec![0x55; env.customization().max_cred_blob_length()]);
|
||||
|
||||
let encrypted_id = encrypt_to_credential_id(
|
||||
&mut env,
|
||||
&private_key,
|
||||
&rp_id_hash,
|
||||
cred_protect_policy,
|
||||
cred_blob,
|
||||
);
|
||||
|
||||
assert!(encrypted_id.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cred_protect_persisted() {
|
||||
let mut env = TestEnv::default();
|
||||
let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
|
||||
|
||||
let rp_id_hash = [0x55; 32];
|
||||
let encrypted_id = encrypt_to_credential_id(
|
||||
&mut env,
|
||||
&private_key,
|
||||
&rp_id_hash,
|
||||
Some(CredentialProtectionPolicy::UserVerificationRequired),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let decrypted_source = decrypt_credential_id(&mut env, encrypted_id, &rp_id_hash)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(decrypted_source.private_key, private_key);
|
||||
assert_eq!(
|
||||
decrypted_source.cred_protect_policy,
|
||||
Some(CredentialProtectionPolicy::UserVerificationRequired)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cred_blob_persisted() {
|
||||
let mut env = TestEnv::default();
|
||||
let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
|
||||
|
||||
let rp_id_hash = [0x55; 32];
|
||||
let cred_blob = Some(vec![0x55; env.customization().max_cred_blob_length()]);
|
||||
let encrypted_id =
|
||||
encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, cred_blob.clone())
|
||||
.unwrap();
|
||||
|
||||
let decrypted_source = decrypt_credential_id(&mut env, encrypted_id, &rp_id_hash)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(decrypted_source.private_key, private_key);
|
||||
assert_eq!(decrypted_source.cred_blob, cred_blob);
|
||||
}
|
||||
}
|
||||
917
libraries/opensk/src/ctap/credential_management.rs
Normal file
917
libraries/opensk/src/ctap/credential_management.rs
Normal file
@@ -0,0 +1,917 @@
|
||||
// Copyright 2020-2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::client_pin::{ClientPin, PinPermission};
|
||||
use super::command::AuthenticatorCredentialManagementParameters;
|
||||
use super::data_formats::{
|
||||
CredentialManagementSubCommand, CredentialManagementSubCommandParameters,
|
||||
PublicKeyCredentialDescriptor, PublicKeyCredentialRpEntity, PublicKeyCredentialSource,
|
||||
PublicKeyCredentialUserEntity,
|
||||
};
|
||||
use super::response::{AuthenticatorCredentialManagementResponse, ResponseData};
|
||||
use super::status_code::Ctap2StatusCode;
|
||||
use super::{Channel, StatefulCommand, StatefulPermission};
|
||||
use crate::ctap::storage;
|
||||
use crate::env::Env;
|
||||
use alloc::collections::BTreeSet;
|
||||
use alloc::string::String;
|
||||
use alloc::vec;
|
||||
use alloc::vec::Vec;
|
||||
use crypto::sha256::Sha256;
|
||||
use crypto::Hash256;
|
||||
|
||||
/// Generates a set with all existing RP IDs.
|
||||
fn get_stored_rp_ids(env: &mut impl Env) -> Result<BTreeSet<String>, Ctap2StatusCode> {
|
||||
let mut rp_set = BTreeSet::new();
|
||||
let mut iter_result = Ok(());
|
||||
for (_, credential) in storage::iter_credentials(env, &mut iter_result)? {
|
||||
rp_set.insert(credential.rp_id);
|
||||
}
|
||||
iter_result?;
|
||||
Ok(rp_set)
|
||||
}
|
||||
|
||||
/// Generates the response for subcommands enumerating RPs.
|
||||
fn enumerate_rps_response(
|
||||
rp_id: String,
|
||||
total_rps: Option<u64>,
|
||||
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
|
||||
let rp_id_hash = Some(Sha256::hash(rp_id.as_bytes()).to_vec());
|
||||
let rp = Some(PublicKeyCredentialRpEntity {
|
||||
rp_id,
|
||||
rp_name: None,
|
||||
rp_icon: None,
|
||||
});
|
||||
Ok(AuthenticatorCredentialManagementResponse {
|
||||
rp,
|
||||
rp_id_hash,
|
||||
total_rps,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
/// Generates the response for subcommands enumerating credentials.
|
||||
fn enumerate_credentials_response(
|
||||
env: &mut impl Env,
|
||||
credential: PublicKeyCredentialSource,
|
||||
total_credentials: Option<u64>,
|
||||
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
|
||||
let PublicKeyCredentialSource {
|
||||
key_type,
|
||||
credential_id,
|
||||
private_key,
|
||||
rp_id: _,
|
||||
user_handle,
|
||||
user_display_name,
|
||||
cred_protect_policy,
|
||||
creation_order: _,
|
||||
user_name,
|
||||
user_icon,
|
||||
cred_blob: _,
|
||||
large_blob_key,
|
||||
} = credential;
|
||||
let user = PublicKeyCredentialUserEntity {
|
||||
user_id: user_handle,
|
||||
user_name,
|
||||
user_display_name,
|
||||
user_icon,
|
||||
};
|
||||
let credential_id = PublicKeyCredentialDescriptor {
|
||||
key_type,
|
||||
key_id: credential_id,
|
||||
transports: None, // You can set USB as a hint here.
|
||||
};
|
||||
let public_key = private_key.get_pub_key(env)?;
|
||||
Ok(AuthenticatorCredentialManagementResponse {
|
||||
user: Some(user),
|
||||
credential_id: Some(credential_id),
|
||||
public_key: Some(public_key),
|
||||
total_credentials,
|
||||
cred_protect: cred_protect_policy,
|
||||
large_blob_key,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if the token permissions have the correct associated RP ID.
|
||||
///
|
||||
/// Either no RP ID is associated, or the RP ID matches the stored credential.
|
||||
fn check_rp_id_permissions<E: Env>(
|
||||
env: &mut E,
|
||||
client_pin: &mut ClientPin<E>,
|
||||
credential_id: &[u8],
|
||||
) -> Result<(), Ctap2StatusCode> {
|
||||
// Pre-check a sufficient condition before calling the store.
|
||||
if client_pin.has_no_rp_id_permission().is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
let (_, credential) = storage::find_credential_item(env, credential_id)?;
|
||||
client_pin.has_no_or_rp_id_permission(&credential.rp_id)
|
||||
}
|
||||
|
||||
/// Processes the subcommand getCredsMetadata for CredentialManagement.
|
||||
fn process_get_creds_metadata(
|
||||
env: &mut impl Env,
|
||||
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
|
||||
Ok(AuthenticatorCredentialManagementResponse {
|
||||
existing_resident_credentials_count: Some(storage::count_credentials(env)? as u64),
|
||||
max_possible_remaining_resident_credentials_count: Some(
|
||||
storage::remaining_credentials(env)? as u64,
|
||||
),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
/// Processes the subcommand enumerateRPsBegin for CredentialManagement.
|
||||
fn process_enumerate_rps_begin<E: Env>(
|
||||
env: &mut E,
|
||||
stateful_command_permission: &mut StatefulPermission<E>,
|
||||
channel: Channel,
|
||||
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
|
||||
let rp_set = get_stored_rp_ids(env)?;
|
||||
let total_rps = rp_set.len();
|
||||
|
||||
if total_rps > 1 {
|
||||
stateful_command_permission.set_command(env, StatefulCommand::EnumerateRps(1), channel);
|
||||
}
|
||||
// TODO https://github.com/rust-lang/rust/issues/62924 replace with pop_first()
|
||||
let rp_id = rp_set
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?;
|
||||
enumerate_rps_response(rp_id, Some(total_rps as u64))
|
||||
}
|
||||
|
||||
/// Processes the subcommand enumerateRPsGetNextRP for CredentialManagement.
|
||||
fn process_enumerate_rps_get_next_rp<E: Env>(
|
||||
env: &mut E,
|
||||
stateful_command_permission: &mut StatefulPermission<E>,
|
||||
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
|
||||
let rp_id_index = stateful_command_permission.next_enumerate_rp(env)?;
|
||||
let rp_set = get_stored_rp_ids(env)?;
|
||||
// A BTreeSet is already sorted.
|
||||
let rp_id = rp_set
|
||||
.into_iter()
|
||||
.nth(rp_id_index)
|
||||
.ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?;
|
||||
enumerate_rps_response(rp_id, None)
|
||||
}
|
||||
|
||||
/// Processes the subcommand enumerateCredentialsBegin for CredentialManagement.
|
||||
fn process_enumerate_credentials_begin<E: Env>(
|
||||
env: &mut E,
|
||||
stateful_command_permission: &mut StatefulPermission<E>,
|
||||
client_pin: &mut ClientPin<E>,
|
||||
sub_command_params: CredentialManagementSubCommandParameters,
|
||||
channel: Channel,
|
||||
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
|
||||
let rp_id_hash = sub_command_params
|
||||
.rp_id_hash
|
||||
.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?;
|
||||
client_pin.has_no_or_rp_id_hash_permission(&rp_id_hash[..])?;
|
||||
let mut iter_result = Ok(());
|
||||
let iter = storage::iter_credentials(env, &mut iter_result)?;
|
||||
let mut rp_credentials: Vec<usize> = iter
|
||||
.filter_map(|(key, credential)| {
|
||||
let cred_rp_id_hash = Sha256::hash(credential.rp_id.as_bytes());
|
||||
if cred_rp_id_hash == rp_id_hash.as_slice() {
|
||||
Some(key)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
iter_result?;
|
||||
let total_credentials = rp_credentials.len();
|
||||
let current_key = rp_credentials
|
||||
.pop()
|
||||
.ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?;
|
||||
let credential = storage::get_credential(env, current_key)?;
|
||||
if total_credentials > 1 {
|
||||
stateful_command_permission.set_command(
|
||||
env,
|
||||
StatefulCommand::EnumerateCredentials(rp_credentials),
|
||||
channel,
|
||||
);
|
||||
}
|
||||
enumerate_credentials_response(env, credential, Some(total_credentials as u64))
|
||||
}
|
||||
|
||||
/// Processes the subcommand enumerateCredentialsGetNextCredential for CredentialManagement.
|
||||
fn process_enumerate_credentials_get_next_credential<E: Env>(
|
||||
env: &mut E,
|
||||
stateful_command_permission: &mut StatefulPermission<E>,
|
||||
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
|
||||
let credential_key = stateful_command_permission.next_enumerate_credential(env)?;
|
||||
let credential = storage::get_credential(env, credential_key)?;
|
||||
enumerate_credentials_response(env, credential, None)
|
||||
}
|
||||
|
||||
/// Processes the subcommand deleteCredential for CredentialManagement.
|
||||
fn process_delete_credential<E: Env>(
|
||||
env: &mut E,
|
||||
client_pin: &mut ClientPin<E>,
|
||||
sub_command_params: CredentialManagementSubCommandParameters,
|
||||
) -> Result<(), Ctap2StatusCode> {
|
||||
let credential_id = sub_command_params
|
||||
.credential_id
|
||||
.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?
|
||||
.key_id;
|
||||
check_rp_id_permissions(env, client_pin, &credential_id)?;
|
||||
storage::delete_credential(env, &credential_id)
|
||||
}
|
||||
|
||||
/// Processes the subcommand updateUserInformation for CredentialManagement.
|
||||
fn process_update_user_information<E: Env>(
|
||||
env: &mut E,
|
||||
client_pin: &mut ClientPin<E>,
|
||||
sub_command_params: CredentialManagementSubCommandParameters,
|
||||
) -> Result<(), Ctap2StatusCode> {
|
||||
let credential_id = sub_command_params
|
||||
.credential_id
|
||||
.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?
|
||||
.key_id;
|
||||
let user = sub_command_params
|
||||
.user
|
||||
.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?;
|
||||
check_rp_id_permissions(env, client_pin, &credential_id)?;
|
||||
storage::update_credential(env, &credential_id, user)
|
||||
}
|
||||
|
||||
/// Processes the CredentialManagement command and all its subcommands.
|
||||
pub fn process_credential_management<E: Env>(
|
||||
env: &mut E,
|
||||
stateful_command_permission: &mut StatefulPermission<E>,
|
||||
client_pin: &mut ClientPin<E>,
|
||||
cred_management_params: AuthenticatorCredentialManagementParameters,
|
||||
channel: Channel,
|
||||
) -> Result<ResponseData, Ctap2StatusCode> {
|
||||
let AuthenticatorCredentialManagementParameters {
|
||||
sub_command,
|
||||
sub_command_params,
|
||||
pin_uv_auth_protocol,
|
||||
pin_uv_auth_param,
|
||||
} = cred_management_params;
|
||||
|
||||
match (sub_command, stateful_command_permission.get_command(env)) {
|
||||
(
|
||||
CredentialManagementSubCommand::EnumerateRpsGetNextRp,
|
||||
Ok(StatefulCommand::EnumerateRps(_)),
|
||||
)
|
||||
| (
|
||||
CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential,
|
||||
Ok(StatefulCommand::EnumerateCredentials(_)),
|
||||
) => (),
|
||||
(_, _) => {
|
||||
stateful_command_permission.clear();
|
||||
}
|
||||
}
|
||||
|
||||
match sub_command {
|
||||
CredentialManagementSubCommand::GetCredsMetadata
|
||||
| CredentialManagementSubCommand::EnumerateRpsBegin
|
||||
| CredentialManagementSubCommand::EnumerateCredentialsBegin
|
||||
| CredentialManagementSubCommand::DeleteCredential
|
||||
| CredentialManagementSubCommand::UpdateUserInformation => {
|
||||
let pin_uv_auth_param =
|
||||
pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?;
|
||||
let pin_uv_auth_protocol =
|
||||
pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?;
|
||||
let mut management_data = vec![sub_command as u8];
|
||||
if let Some(sub_command_params) = sub_command_params.clone() {
|
||||
super::cbor_write(sub_command_params.into(), &mut management_data)?;
|
||||
}
|
||||
client_pin.verify_pin_uv_auth_token(
|
||||
&management_data,
|
||||
&pin_uv_auth_param,
|
||||
pin_uv_auth_protocol,
|
||||
)?;
|
||||
// The RP ID permission is handled differently per subcommand below.
|
||||
client_pin.has_permission(PinPermission::CredentialManagement)?;
|
||||
}
|
||||
CredentialManagementSubCommand::EnumerateRpsGetNextRp
|
||||
| CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential => {}
|
||||
}
|
||||
|
||||
let response = match sub_command {
|
||||
CredentialManagementSubCommand::GetCredsMetadata => {
|
||||
client_pin.has_no_rp_id_permission()?;
|
||||
Some(process_get_creds_metadata(env)?)
|
||||
}
|
||||
CredentialManagementSubCommand::EnumerateRpsBegin => {
|
||||
client_pin.has_no_rp_id_permission()?;
|
||||
Some(process_enumerate_rps_begin(
|
||||
env,
|
||||
stateful_command_permission,
|
||||
channel,
|
||||
)?)
|
||||
}
|
||||
CredentialManagementSubCommand::EnumerateRpsGetNextRp => Some(
|
||||
process_enumerate_rps_get_next_rp(env, stateful_command_permission)?,
|
||||
),
|
||||
CredentialManagementSubCommand::EnumerateCredentialsBegin => {
|
||||
Some(process_enumerate_credentials_begin(
|
||||
env,
|
||||
stateful_command_permission,
|
||||
client_pin,
|
||||
sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
|
||||
channel,
|
||||
)?)
|
||||
}
|
||||
CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential => Some(
|
||||
process_enumerate_credentials_get_next_credential(env, stateful_command_permission)?,
|
||||
),
|
||||
CredentialManagementSubCommand::DeleteCredential => {
|
||||
process_delete_credential(
|
||||
env,
|
||||
client_pin,
|
||||
sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
|
||||
)?;
|
||||
None
|
||||
}
|
||||
CredentialManagementSubCommand::UpdateUserInformation => {
|
||||
process_update_user_information(
|
||||
env,
|
||||
client_pin,
|
||||
sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
|
||||
)?;
|
||||
None
|
||||
}
|
||||
};
|
||||
Ok(ResponseData::AuthenticatorCredentialManagement(response))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::super::crypto_wrapper::PrivateKey;
|
||||
use super::super::data_formats::{PinUvAuthProtocol, PublicKeyCredentialType};
|
||||
use super::super::pin_protocol::authenticate_pin_uv_auth_token;
|
||||
use super::super::CtapState;
|
||||
use super::*;
|
||||
use crate::env::test::TestEnv;
|
||||
use rng256::Rng256;
|
||||
|
||||
const DUMMY_CHANNEL: Channel = Channel::MainHid([0x12, 0x34, 0x56, 0x78]);
|
||||
|
||||
fn create_credential_source(env: &mut TestEnv) -> PublicKeyCredentialSource {
|
||||
let private_key = PrivateKey::new_ecdsa(env);
|
||||
PublicKeyCredentialSource {
|
||||
key_type: PublicKeyCredentialType::PublicKey,
|
||||
credential_id: env.rng().gen_uniform_u8x32().to_vec(),
|
||||
private_key,
|
||||
rp_id: String::from("example.com"),
|
||||
user_handle: vec![0x01],
|
||||
user_display_name: Some("display_name".to_string()),
|
||||
cred_protect_policy: None,
|
||||
creation_order: 0,
|
||||
user_name: Some("name".to_string()),
|
||||
user_icon: Some("icon".to_string()),
|
||||
cred_blob: None,
|
||||
large_blob_key: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn test_helper_process_get_creds_metadata(pin_uv_auth_protocol: PinUvAuthProtocol) {
|
||||
let mut env = TestEnv::default();
|
||||
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
|
||||
let pin_uv_auth_token = [0x55; 32];
|
||||
let client_pin = ClientPin::<TestEnv>::new_test(
|
||||
&mut env,
|
||||
key_agreement_key,
|
||||
pin_uv_auth_token,
|
||||
pin_uv_auth_protocol,
|
||||
);
|
||||
let credential_source = create_credential_source(&mut env);
|
||||
|
||||
let mut ctap_state = CtapState::new(&mut env);
|
||||
ctap_state.client_pin = client_pin;
|
||||
|
||||
storage::set_pin(&mut env, &[0u8; 16], 4).unwrap();
|
||||
let management_data = vec![CredentialManagementSubCommand::GetCredsMetadata as u8];
|
||||
let pin_uv_auth_param = authenticate_pin_uv_auth_token(
|
||||
&pin_uv_auth_token,
|
||||
&management_data,
|
||||
pin_uv_auth_protocol,
|
||||
);
|
||||
|
||||
let cred_management_params = AuthenticatorCredentialManagementParameters {
|
||||
sub_command: CredentialManagementSubCommand::GetCredsMetadata,
|
||||
sub_command_params: None,
|
||||
pin_uv_auth_protocol: Some(pin_uv_auth_protocol),
|
||||
pin_uv_auth_param: Some(pin_uv_auth_param.clone()),
|
||||
};
|
||||
let cred_management_response = process_credential_management(
|
||||
&mut env,
|
||||
&mut ctap_state.stateful_command_permission,
|
||||
&mut ctap_state.client_pin,
|
||||
cred_management_params,
|
||||
DUMMY_CHANNEL,
|
||||
);
|
||||
let initial_capacity = match cred_management_response.unwrap() {
|
||||
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
|
||||
assert_eq!(response.existing_resident_credentials_count, Some(0));
|
||||
response
|
||||
.max_possible_remaining_resident_credentials_count
|
||||
.unwrap()
|
||||
}
|
||||
_ => panic!("Invalid response type"),
|
||||
};
|
||||
|
||||
storage::store_credential(&mut env, credential_source).unwrap();
|
||||
|
||||
let cred_management_params = AuthenticatorCredentialManagementParameters {
|
||||
sub_command: CredentialManagementSubCommand::GetCredsMetadata,
|
||||
sub_command_params: None,
|
||||
pin_uv_auth_protocol: Some(pin_uv_auth_protocol),
|
||||
pin_uv_auth_param: Some(pin_uv_auth_param),
|
||||
};
|
||||
let cred_management_response = process_credential_management(
|
||||
&mut env,
|
||||
&mut ctap_state.stateful_command_permission,
|
||||
&mut ctap_state.client_pin,
|
||||
cred_management_params,
|
||||
DUMMY_CHANNEL,
|
||||
);
|
||||
match cred_management_response.unwrap() {
|
||||
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
|
||||
assert_eq!(response.existing_resident_credentials_count, Some(1));
|
||||
assert_eq!(
|
||||
response.max_possible_remaining_resident_credentials_count,
|
||||
Some(initial_capacity - 1)
|
||||
);
|
||||
}
|
||||
_ => panic!("Invalid response type"),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_get_creds_metadata_v1() {
|
||||
test_helper_process_get_creds_metadata(PinUvAuthProtocol::V1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_get_creds_metadata_v2() {
|
||||
test_helper_process_get_creds_metadata(PinUvAuthProtocol::V2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_enumerate_rps_with_uv() {
|
||||
let mut env = TestEnv::default();
|
||||
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
|
||||
let pin_uv_auth_token = [0x55; 32];
|
||||
let client_pin = ClientPin::<TestEnv>::new_test(
|
||||
&mut env,
|
||||
key_agreement_key,
|
||||
pin_uv_auth_token,
|
||||
PinUvAuthProtocol::V1,
|
||||
);
|
||||
let credential_source1 = create_credential_source(&mut env);
|
||||
let mut credential_source2 = create_credential_source(&mut env);
|
||||
credential_source2.rp_id = "another.example.com".to_string();
|
||||
|
||||
let mut ctap_state = CtapState::new(&mut env);
|
||||
ctap_state.client_pin = client_pin;
|
||||
|
||||
storage::store_credential(&mut env, credential_source1).unwrap();
|
||||
storage::store_credential(&mut env, credential_source2).unwrap();
|
||||
|
||||
storage::set_pin(&mut env, &[0u8; 16], 4).unwrap();
|
||||
let pin_uv_auth_param = Some(vec![
|
||||
0x1A, 0xA4, 0x96, 0xDA, 0x62, 0x80, 0x28, 0x13, 0xEB, 0x32, 0xB9, 0xF1, 0xD2, 0xA9,
|
||||
0xD0, 0xD1,
|
||||
]);
|
||||
|
||||
let cred_management_params = AuthenticatorCredentialManagementParameters {
|
||||
sub_command: CredentialManagementSubCommand::EnumerateRpsBegin,
|
||||
sub_command_params: None,
|
||||
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
|
||||
pin_uv_auth_param,
|
||||
};
|
||||
let cred_management_response = process_credential_management(
|
||||
&mut env,
|
||||
&mut ctap_state.stateful_command_permission,
|
||||
&mut ctap_state.client_pin,
|
||||
cred_management_params,
|
||||
DUMMY_CHANNEL,
|
||||
);
|
||||
let first_rp_id = match cred_management_response.unwrap() {
|
||||
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
|
||||
assert_eq!(response.total_rps, Some(2));
|
||||
let rp_id = response.rp.unwrap().rp_id;
|
||||
let rp_id_hash = Sha256::hash(rp_id.as_bytes());
|
||||
assert_eq!(rp_id_hash, response.rp_id_hash.unwrap().as_slice());
|
||||
rp_id
|
||||
}
|
||||
_ => panic!("Invalid response type"),
|
||||
};
|
||||
|
||||
let cred_management_params = AuthenticatorCredentialManagementParameters {
|
||||
sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp,
|
||||
sub_command_params: None,
|
||||
pin_uv_auth_protocol: None,
|
||||
pin_uv_auth_param: None,
|
||||
};
|
||||
let cred_management_response = process_credential_management(
|
||||
&mut env,
|
||||
&mut ctap_state.stateful_command_permission,
|
||||
&mut ctap_state.client_pin,
|
||||
cred_management_params,
|
||||
DUMMY_CHANNEL,
|
||||
);
|
||||
let second_rp_id = match cred_management_response.unwrap() {
|
||||
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
|
||||
assert_eq!(response.total_rps, None);
|
||||
let rp_id = response.rp.unwrap().rp_id;
|
||||
let rp_id_hash = Sha256::hash(rp_id.as_bytes());
|
||||
assert_eq!(rp_id_hash, response.rp_id_hash.unwrap().as_slice());
|
||||
rp_id
|
||||
}
|
||||
_ => panic!("Invalid response type"),
|
||||
};
|
||||
|
||||
assert!(first_rp_id != second_rp_id);
|
||||
let cred_management_params = AuthenticatorCredentialManagementParameters {
|
||||
sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp,
|
||||
sub_command_params: None,
|
||||
pin_uv_auth_protocol: None,
|
||||
pin_uv_auth_param: None,
|
||||
};
|
||||
let cred_management_response = process_credential_management(
|
||||
&mut env,
|
||||
&mut ctap_state.stateful_command_permission,
|
||||
&mut ctap_state.client_pin,
|
||||
cred_management_params,
|
||||
DUMMY_CHANNEL,
|
||||
);
|
||||
assert_eq!(
|
||||
cred_management_response,
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_enumerate_rps_completeness() {
|
||||
let mut env = TestEnv::default();
|
||||
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
|
||||
let pin_uv_auth_token = [0x55; 32];
|
||||
let client_pin = ClientPin::<TestEnv>::new_test(
|
||||
&mut env,
|
||||
key_agreement_key,
|
||||
pin_uv_auth_token,
|
||||
PinUvAuthProtocol::V1,
|
||||
);
|
||||
let credential_source = create_credential_source(&mut env);
|
||||
|
||||
let mut ctap_state = CtapState::new(&mut env);
|
||||
ctap_state.client_pin = client_pin;
|
||||
|
||||
const NUM_CREDENTIALS: usize = 20;
|
||||
for i in 0..NUM_CREDENTIALS {
|
||||
let mut credential = credential_source.clone();
|
||||
credential.rp_id = i.to_string();
|
||||
storage::store_credential(&mut env, credential).unwrap();
|
||||
}
|
||||
|
||||
storage::set_pin(&mut env, &[0u8; 16], 4).unwrap();
|
||||
let pin_uv_auth_param = Some(vec![
|
||||
0x1A, 0xA4, 0x96, 0xDA, 0x62, 0x80, 0x28, 0x13, 0xEB, 0x32, 0xB9, 0xF1, 0xD2, 0xA9,
|
||||
0xD0, 0xD1,
|
||||
]);
|
||||
|
||||
let mut rp_set = BTreeSet::new();
|
||||
// This mut is just to make the test code shorter.
|
||||
// The command is different on the first loop iteration.
|
||||
let mut cred_management_params = AuthenticatorCredentialManagementParameters {
|
||||
sub_command: CredentialManagementSubCommand::EnumerateRpsBegin,
|
||||
sub_command_params: None,
|
||||
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
|
||||
pin_uv_auth_param,
|
||||
};
|
||||
|
||||
for _ in 0..NUM_CREDENTIALS {
|
||||
let cred_management_response = process_credential_management(
|
||||
&mut env,
|
||||
&mut ctap_state.stateful_command_permission,
|
||||
&mut ctap_state.client_pin,
|
||||
cred_management_params,
|
||||
DUMMY_CHANNEL,
|
||||
);
|
||||
match cred_management_response.unwrap() {
|
||||
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
|
||||
if rp_set.is_empty() {
|
||||
assert_eq!(response.total_rps, Some(NUM_CREDENTIALS as u64));
|
||||
} else {
|
||||
assert_eq!(response.total_rps, None);
|
||||
}
|
||||
let rp_id = response.rp.unwrap().rp_id;
|
||||
let rp_id_hash = Sha256::hash(rp_id.as_bytes());
|
||||
assert_eq!(rp_id_hash, response.rp_id_hash.unwrap().as_slice());
|
||||
assert!(!rp_set.contains(&rp_id));
|
||||
rp_set.insert(rp_id);
|
||||
}
|
||||
_ => panic!("Invalid response type"),
|
||||
};
|
||||
cred_management_params = AuthenticatorCredentialManagementParameters {
|
||||
sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp,
|
||||
sub_command_params: None,
|
||||
pin_uv_auth_protocol: None,
|
||||
pin_uv_auth_param: None,
|
||||
};
|
||||
}
|
||||
|
||||
let cred_management_response = process_credential_management(
|
||||
&mut env,
|
||||
&mut ctap_state.stateful_command_permission,
|
||||
&mut ctap_state.client_pin,
|
||||
cred_management_params,
|
||||
DUMMY_CHANNEL,
|
||||
);
|
||||
assert_eq!(
|
||||
cred_management_response,
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_enumerate_credentials_with_uv() {
|
||||
let mut env = TestEnv::default();
|
||||
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
|
||||
let pin_uv_auth_token = [0x55; 32];
|
||||
let client_pin = ClientPin::<TestEnv>::new_test(
|
||||
&mut env,
|
||||
key_agreement_key,
|
||||
pin_uv_auth_token,
|
||||
PinUvAuthProtocol::V1,
|
||||
);
|
||||
let credential_source1 = create_credential_source(&mut env);
|
||||
let mut credential_source2 = create_credential_source(&mut env);
|
||||
credential_source2.user_handle = vec![0x02];
|
||||
credential_source2.user_name = Some("user2".to_string());
|
||||
credential_source2.user_display_name = Some("User Two".to_string());
|
||||
credential_source2.user_icon = Some("icon2".to_string());
|
||||
|
||||
let mut ctap_state = CtapState::new(&mut env);
|
||||
ctap_state.client_pin = client_pin;
|
||||
|
||||
storage::store_credential(&mut env, credential_source1).unwrap();
|
||||
storage::store_credential(&mut env, credential_source2).unwrap();
|
||||
|
||||
storage::set_pin(&mut env, &[0u8; 16], 4).unwrap();
|
||||
let pin_uv_auth_param = Some(vec![
|
||||
0xF8, 0xB0, 0x3C, 0xC1, 0xD5, 0x58, 0x9C, 0xB7, 0x4D, 0x42, 0xA1, 0x64, 0x14, 0x28,
|
||||
0x2B, 0x68,
|
||||
]);
|
||||
|
||||
let sub_command_params = CredentialManagementSubCommandParameters {
|
||||
rp_id_hash: Some(Sha256::hash(b"example.com").to_vec()),
|
||||
credential_id: None,
|
||||
user: None,
|
||||
};
|
||||
// RP ID hash:
|
||||
// A379A6F6EEAFB9A55E378C118034E2751E682FAB9F2D30AB13D2125586CE1947
|
||||
let cred_management_params = AuthenticatorCredentialManagementParameters {
|
||||
sub_command: CredentialManagementSubCommand::EnumerateCredentialsBegin,
|
||||
sub_command_params: Some(sub_command_params),
|
||||
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
|
||||
pin_uv_auth_param,
|
||||
};
|
||||
let cred_management_response = process_credential_management(
|
||||
&mut env,
|
||||
&mut ctap_state.stateful_command_permission,
|
||||
&mut ctap_state.client_pin,
|
||||
cred_management_params,
|
||||
DUMMY_CHANNEL,
|
||||
);
|
||||
let first_credential_id = match cred_management_response.unwrap() {
|
||||
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
|
||||
assert!(response.user.is_some());
|
||||
assert!(response.public_key.is_some());
|
||||
assert_eq!(response.total_credentials, Some(2));
|
||||
response.credential_id.unwrap().key_id
|
||||
}
|
||||
_ => panic!("Invalid response type"),
|
||||
};
|
||||
|
||||
let cred_management_params = AuthenticatorCredentialManagementParameters {
|
||||
sub_command: CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential,
|
||||
sub_command_params: None,
|
||||
pin_uv_auth_protocol: None,
|
||||
pin_uv_auth_param: None,
|
||||
};
|
||||
let cred_management_response = process_credential_management(
|
||||
&mut env,
|
||||
&mut ctap_state.stateful_command_permission,
|
||||
&mut ctap_state.client_pin,
|
||||
cred_management_params,
|
||||
DUMMY_CHANNEL,
|
||||
);
|
||||
let second_credential_id = match cred_management_response.unwrap() {
|
||||
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
|
||||
assert!(response.user.is_some());
|
||||
assert!(response.public_key.is_some());
|
||||
assert_eq!(response.total_credentials, None);
|
||||
response.credential_id.unwrap().key_id
|
||||
}
|
||||
_ => panic!("Invalid response type"),
|
||||
};
|
||||
|
||||
assert!(first_credential_id != second_credential_id);
|
||||
let cred_management_params = AuthenticatorCredentialManagementParameters {
|
||||
sub_command: CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential,
|
||||
sub_command_params: None,
|
||||
pin_uv_auth_protocol: None,
|
||||
pin_uv_auth_param: None,
|
||||
};
|
||||
let cred_management_response = process_credential_management(
|
||||
&mut env,
|
||||
&mut ctap_state.stateful_command_permission,
|
||||
&mut ctap_state.client_pin,
|
||||
cred_management_params,
|
||||
DUMMY_CHANNEL,
|
||||
);
|
||||
assert_eq!(
|
||||
cred_management_response,
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_delete_credential() {
|
||||
let mut env = TestEnv::default();
|
||||
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
|
||||
let pin_uv_auth_token = [0x55; 32];
|
||||
let client_pin = ClientPin::<TestEnv>::new_test(
|
||||
&mut env,
|
||||
key_agreement_key,
|
||||
pin_uv_auth_token,
|
||||
PinUvAuthProtocol::V1,
|
||||
);
|
||||
let mut credential_source = create_credential_source(&mut env);
|
||||
credential_source.credential_id = vec![0x1D; 32];
|
||||
|
||||
let mut ctap_state = CtapState::new(&mut env);
|
||||
ctap_state.client_pin = client_pin;
|
||||
|
||||
storage::store_credential(&mut env, credential_source).unwrap();
|
||||
|
||||
storage::set_pin(&mut env, &[0u8; 16], 4).unwrap();
|
||||
let pin_uv_auth_param = Some(vec![
|
||||
0xBD, 0xE3, 0xEF, 0x8A, 0x77, 0x01, 0xB1, 0x69, 0x19, 0xE6, 0x62, 0xB9, 0x9B, 0x89,
|
||||
0x9C, 0x64,
|
||||
]);
|
||||
|
||||
let credential_id = PublicKeyCredentialDescriptor {
|
||||
key_type: PublicKeyCredentialType::PublicKey,
|
||||
key_id: vec![0x1D; 32],
|
||||
transports: None, // You can set USB as a hint here.
|
||||
};
|
||||
let sub_command_params = CredentialManagementSubCommandParameters {
|
||||
rp_id_hash: None,
|
||||
credential_id: Some(credential_id),
|
||||
user: None,
|
||||
};
|
||||
let cred_management_params = AuthenticatorCredentialManagementParameters {
|
||||
sub_command: CredentialManagementSubCommand::DeleteCredential,
|
||||
sub_command_params: Some(sub_command_params.clone()),
|
||||
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
|
||||
pin_uv_auth_param: pin_uv_auth_param.clone(),
|
||||
};
|
||||
let cred_management_response = process_credential_management(
|
||||
&mut env,
|
||||
&mut ctap_state.stateful_command_permission,
|
||||
&mut ctap_state.client_pin,
|
||||
cred_management_params,
|
||||
DUMMY_CHANNEL,
|
||||
);
|
||||
assert_eq!(
|
||||
cred_management_response,
|
||||
Ok(ResponseData::AuthenticatorCredentialManagement(None))
|
||||
);
|
||||
|
||||
let cred_management_params = AuthenticatorCredentialManagementParameters {
|
||||
sub_command: CredentialManagementSubCommand::DeleteCredential,
|
||||
sub_command_params: Some(sub_command_params),
|
||||
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
|
||||
pin_uv_auth_param,
|
||||
};
|
||||
let cred_management_response = process_credential_management(
|
||||
&mut env,
|
||||
&mut ctap_state.stateful_command_permission,
|
||||
&mut ctap_state.client_pin,
|
||||
cred_management_params,
|
||||
DUMMY_CHANNEL,
|
||||
);
|
||||
assert_eq!(
|
||||
cred_management_response,
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_update_user_information() {
|
||||
let mut env = TestEnv::default();
|
||||
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
|
||||
let pin_uv_auth_token = [0x55; 32];
|
||||
let client_pin = ClientPin::<TestEnv>::new_test(
|
||||
&mut env,
|
||||
key_agreement_key,
|
||||
pin_uv_auth_token,
|
||||
PinUvAuthProtocol::V1,
|
||||
);
|
||||
let mut credential_source = create_credential_source(&mut env);
|
||||
credential_source.credential_id = vec![0x1D; 32];
|
||||
|
||||
let mut ctap_state = CtapState::new(&mut env);
|
||||
ctap_state.client_pin = client_pin;
|
||||
|
||||
storage::store_credential(&mut env, credential_source).unwrap();
|
||||
|
||||
storage::set_pin(&mut env, &[0u8; 16], 4).unwrap();
|
||||
let pin_uv_auth_param = Some(vec![
|
||||
0xA5, 0x55, 0x8F, 0x03, 0xC3, 0xD3, 0x73, 0x1C, 0x07, 0xDA, 0x1F, 0x8C, 0xC7, 0xBD,
|
||||
0x9D, 0xB7,
|
||||
]);
|
||||
|
||||
let credential_id = PublicKeyCredentialDescriptor {
|
||||
key_type: PublicKeyCredentialType::PublicKey,
|
||||
key_id: vec![0x1D; 32],
|
||||
transports: None, // You can set USB as a hint here.
|
||||
};
|
||||
let new_user = PublicKeyCredentialUserEntity {
|
||||
user_id: vec![0xFF],
|
||||
user_name: Some("new_name".to_string()),
|
||||
user_display_name: Some("new_display_name".to_string()),
|
||||
user_icon: Some("new_icon".to_string()),
|
||||
};
|
||||
let sub_command_params = CredentialManagementSubCommandParameters {
|
||||
rp_id_hash: None,
|
||||
credential_id: Some(credential_id),
|
||||
user: Some(new_user),
|
||||
};
|
||||
let cred_management_params = AuthenticatorCredentialManagementParameters {
|
||||
sub_command: CredentialManagementSubCommand::UpdateUserInformation,
|
||||
sub_command_params: Some(sub_command_params),
|
||||
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
|
||||
pin_uv_auth_param,
|
||||
};
|
||||
let cred_management_response = process_credential_management(
|
||||
&mut env,
|
||||
&mut ctap_state.stateful_command_permission,
|
||||
&mut ctap_state.client_pin,
|
||||
cred_management_params,
|
||||
DUMMY_CHANNEL,
|
||||
);
|
||||
assert_eq!(
|
||||
cred_management_response,
|
||||
Ok(ResponseData::AuthenticatorCredentialManagement(None))
|
||||
);
|
||||
|
||||
let updated_credential = storage::find_credential(&mut env, "example.com", &[0x1D; 32])
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(updated_credential.user_handle, vec![0x01]);
|
||||
assert_eq!(&updated_credential.user_name.unwrap(), "new_name");
|
||||
assert_eq!(
|
||||
&updated_credential.user_display_name.unwrap(),
|
||||
"new_display_name"
|
||||
);
|
||||
assert_eq!(&updated_credential.user_icon.unwrap(), "new_icon");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_credential_management_invalid_pin_uv_auth_param() {
|
||||
let mut env = TestEnv::default();
|
||||
let mut ctap_state = CtapState::new(&mut env);
|
||||
|
||||
storage::set_pin(&mut env, &[0u8; 16], 4).unwrap();
|
||||
|
||||
let cred_management_params = AuthenticatorCredentialManagementParameters {
|
||||
sub_command: CredentialManagementSubCommand::GetCredsMetadata,
|
||||
sub_command_params: None,
|
||||
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
|
||||
pin_uv_auth_param: Some(vec![0u8; 16]),
|
||||
};
|
||||
let cred_management_response = process_credential_management(
|
||||
&mut env,
|
||||
&mut ctap_state.stateful_command_permission,
|
||||
&mut ctap_state.client_pin,
|
||||
cred_management_params,
|
||||
DUMMY_CHANNEL,
|
||||
);
|
||||
assert_eq!(
|
||||
cred_management_response,
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
|
||||
);
|
||||
}
|
||||
}
|
||||
428
libraries/opensk/src/ctap/crypto_wrapper.rs
Normal file
428
libraries/opensk/src/ctap/crypto_wrapper.rs
Normal file
@@ -0,0 +1,428 @@
|
||||
// Copyright 2021-2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::api::key_store::KeyStore;
|
||||
use crate::ctap::data_formats::{extract_array, extract_byte_string, CoseKey, SignatureAlgorithm};
|
||||
use crate::ctap::status_code::Ctap2StatusCode;
|
||||
use crate::env::Env;
|
||||
use alloc::vec;
|
||||
use alloc::vec::Vec;
|
||||
use core::convert::TryFrom;
|
||||
use crypto::cbc::{cbc_decrypt, cbc_encrypt};
|
||||
use crypto::ecdsa;
|
||||
use crypto::sha256::Sha256;
|
||||
use rng256::Rng256;
|
||||
use sk_cbor as cbor;
|
||||
use sk_cbor::{cbor_array, cbor_bytes, cbor_int};
|
||||
|
||||
/// Wraps the AES256-CBC encryption to match what we need in CTAP.
|
||||
pub fn aes256_cbc_encrypt(
|
||||
rng: &mut dyn Rng256,
|
||||
aes_enc_key: &crypto::aes256::EncryptionKey,
|
||||
plaintext: &[u8],
|
||||
embeds_iv: bool,
|
||||
) -> Result<Vec<u8>, Ctap2StatusCode> {
|
||||
if plaintext.len() % 16 != 0 {
|
||||
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
|
||||
}
|
||||
// The extra 1 capacity is because encrypt_key_handle adds a version number.
|
||||
let mut ciphertext = Vec::with_capacity(plaintext.len() + 16 * embeds_iv as usize + 1);
|
||||
let iv = if embeds_iv {
|
||||
let random_bytes = rng.gen_uniform_u8x32();
|
||||
ciphertext.extend_from_slice(&random_bytes[..16]);
|
||||
*array_ref!(ciphertext, 0, 16)
|
||||
} else {
|
||||
[0u8; 16]
|
||||
};
|
||||
let start = ciphertext.len();
|
||||
ciphertext.extend_from_slice(plaintext);
|
||||
cbc_encrypt(aes_enc_key, iv, &mut ciphertext[start..]);
|
||||
Ok(ciphertext)
|
||||
}
|
||||
|
||||
/// Wraps the AES256-CBC decryption to match what we need in CTAP.
|
||||
pub fn aes256_cbc_decrypt(
|
||||
aes_enc_key: &crypto::aes256::EncryptionKey,
|
||||
ciphertext: &[u8],
|
||||
embeds_iv: bool,
|
||||
) -> Result<Vec<u8>, Ctap2StatusCode> {
|
||||
if ciphertext.len() % 16 != 0 || (embeds_iv && ciphertext.is_empty()) {
|
||||
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
|
||||
}
|
||||
let (iv, ciphertext) = if embeds_iv {
|
||||
let (iv, ciphertext) = ciphertext.split_at(16);
|
||||
(*array_ref!(iv, 0, 16), ciphertext)
|
||||
} else {
|
||||
([0u8; 16], ciphertext)
|
||||
};
|
||||
let mut plaintext = ciphertext.to_vec();
|
||||
let aes_dec_key = crypto::aes256::DecryptionKey::new(aes_enc_key);
|
||||
cbc_decrypt(&aes_dec_key, iv, &mut plaintext);
|
||||
Ok(plaintext)
|
||||
}
|
||||
|
||||
/// An asymmetric private key that can sign messages.
|
||||
#[derive(Clone, Debug)]
|
||||
// We shouldn't compare private keys in prod without constant-time operations.
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
pub enum PrivateKey {
|
||||
// We store the seed instead of the key since we can't get the seed back from the key. We could
|
||||
// store both if we believe deriving the key is done more than once and costly.
|
||||
Ecdsa([u8; 32]),
|
||||
#[cfg(feature = "ed25519")]
|
||||
Ed25519(ed25519_compact::SecretKey),
|
||||
}
|
||||
|
||||
impl PrivateKey {
|
||||
/// Creates a new private key for the given algorithm.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the algorithm is [`SignatureAlgorithm::Unknown`].
|
||||
pub fn new(env: &mut impl Env, alg: SignatureAlgorithm) -> Self {
|
||||
match alg {
|
||||
SignatureAlgorithm::Es256 => {
|
||||
PrivateKey::Ecdsa(env.key_store().generate_ecdsa_seed().unwrap())
|
||||
}
|
||||
#[cfg(feature = "ed25519")]
|
||||
SignatureAlgorithm::Eddsa => {
|
||||
let bytes = env.rng().gen_uniform_u8x32();
|
||||
Self::new_ed25519_from_bytes(&bytes).unwrap()
|
||||
}
|
||||
SignatureAlgorithm::Unknown => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new ecdsa private key.
|
||||
pub fn new_ecdsa(env: &mut impl Env) -> PrivateKey {
|
||||
Self::new(env, SignatureAlgorithm::Es256)
|
||||
}
|
||||
|
||||
/// Helper function that creates a private key of type ECDSA.
|
||||
///
|
||||
/// This function is public for legacy credential source parsing only.
|
||||
pub fn new_ecdsa_from_bytes(bytes: &[u8]) -> Option<Self> {
|
||||
if bytes.len() != 32 {
|
||||
return None;
|
||||
}
|
||||
Some(PrivateKey::Ecdsa(*array_ref!(bytes, 0, 32)))
|
||||
}
|
||||
|
||||
#[cfg(feature = "ed25519")]
|
||||
pub fn new_ed25519_from_bytes(bytes: &[u8]) -> Option<Self> {
|
||||
if bytes.len() != 32 {
|
||||
return None;
|
||||
}
|
||||
let seed = ed25519_compact::Seed::from_slice(bytes).unwrap();
|
||||
Some(Self::Ed25519(ed25519_compact::KeyPair::from_seed(seed).sk))
|
||||
}
|
||||
|
||||
/// Returns the ECDSA private key.
|
||||
pub fn ecdsa_key(&self, env: &mut impl Env) -> Result<ecdsa::SecKey, Ctap2StatusCode> {
|
||||
match self {
|
||||
PrivateKey::Ecdsa(seed) => ecdsa_key_from_seed(env, seed),
|
||||
#[allow(unreachable_patterns)]
|
||||
_ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the corresponding public key.
|
||||
pub fn get_pub_key(&self, env: &mut impl Env) -> Result<CoseKey, Ctap2StatusCode> {
|
||||
Ok(match self {
|
||||
PrivateKey::Ecdsa(ecdsa_seed) => {
|
||||
CoseKey::from(ecdsa_key_from_seed(env, ecdsa_seed)?.genpk())
|
||||
}
|
||||
#[cfg(feature = "ed25519")]
|
||||
PrivateKey::Ed25519(ed25519_key) => CoseKey::from(ed25519_key.public_key()),
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the encoded signature for a given message.
|
||||
pub fn sign_and_encode(
|
||||
&self,
|
||||
env: &mut impl Env,
|
||||
message: &[u8],
|
||||
) -> Result<Vec<u8>, Ctap2StatusCode> {
|
||||
Ok(match self {
|
||||
PrivateKey::Ecdsa(ecdsa_seed) => ecdsa_key_from_seed(env, ecdsa_seed)?
|
||||
.sign_rfc6979::<Sha256>(message)
|
||||
.to_asn1_der(),
|
||||
#[cfg(feature = "ed25519")]
|
||||
PrivateKey::Ed25519(ed25519_key) => ed25519_key.sign(message, None).to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
/// The associated COSE signature algorithm identifier.
|
||||
pub fn signature_algorithm(&self) -> SignatureAlgorithm {
|
||||
match self {
|
||||
PrivateKey::Ecdsa(_) => SignatureAlgorithm::Es256,
|
||||
#[cfg(feature = "ed25519")]
|
||||
PrivateKey::Ed25519(_) => SignatureAlgorithm::Eddsa,
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes the key bytes.
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
match self {
|
||||
PrivateKey::Ecdsa(ecdsa_seed) => ecdsa_seed.to_vec(),
|
||||
#[cfg(feature = "ed25519")]
|
||||
PrivateKey::Ed25519(ed25519_key) => ed25519_key.seed().to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ecdsa_key_from_seed(
|
||||
env: &mut impl Env,
|
||||
seed: &[u8; 32],
|
||||
) -> Result<ecdsa::SecKey, Ctap2StatusCode> {
|
||||
let ecdsa_bytes = env.key_store().derive_ecdsa(seed)?;
|
||||
Ok(ecdsa::SecKey::from_bytes(&ecdsa_bytes).unwrap())
|
||||
}
|
||||
|
||||
impl From<&PrivateKey> for cbor::Value {
|
||||
fn from(private_key: &PrivateKey) -> Self {
|
||||
cbor_array![
|
||||
cbor_int!(private_key.signature_algorithm() as i64),
|
||||
cbor_bytes!(private_key.to_bytes()),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<cbor::Value> for PrivateKey {
|
||||
type Error = Ctap2StatusCode;
|
||||
|
||||
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
|
||||
let mut array = extract_array(cbor_value)?;
|
||||
if array.len() != 2 {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR);
|
||||
}
|
||||
let key_bytes = extract_byte_string(array.pop().unwrap())?;
|
||||
match SignatureAlgorithm::try_from(array.pop().unwrap())? {
|
||||
SignatureAlgorithm::Es256 => PrivateKey::new_ecdsa_from_bytes(&key_bytes)
|
||||
.ok_or(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR),
|
||||
#[cfg(feature = "ed25519")]
|
||||
SignatureAlgorithm::Eddsa => PrivateKey::new_ed25519_from_bytes(&key_bytes)
|
||||
.ok_or(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR),
|
||||
_ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::env::test::TestEnv;
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_decrypt_with_iv() {
|
||||
let mut env = TestEnv::default();
|
||||
let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]);
|
||||
let plaintext = vec![0xAA; 64];
|
||||
let ciphertext = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, true).unwrap();
|
||||
let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext, true).unwrap();
|
||||
assert_eq!(decrypted, plaintext);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_decrypt_without_iv() {
|
||||
let mut env = TestEnv::default();
|
||||
let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]);
|
||||
let plaintext = vec![0xAA; 64];
|
||||
let ciphertext = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, false).unwrap();
|
||||
let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext, false).unwrap();
|
||||
assert_eq!(decrypted, plaintext);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_correct_iv_usage() {
|
||||
let mut env = TestEnv::default();
|
||||
let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]);
|
||||
let plaintext = vec![0xAA; 64];
|
||||
let mut ciphertext_no_iv =
|
||||
aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, false).unwrap();
|
||||
let mut ciphertext_with_iv = vec![0u8; 16];
|
||||
ciphertext_with_iv.append(&mut ciphertext_no_iv);
|
||||
let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext_with_iv, true).unwrap();
|
||||
assert_eq!(decrypted, plaintext);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iv_manipulation_property() {
|
||||
let mut env = TestEnv::default();
|
||||
let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]);
|
||||
let plaintext = vec![0xAA; 64];
|
||||
let mut ciphertext = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, true).unwrap();
|
||||
let mut expected_plaintext = plaintext;
|
||||
for i in 0..16 {
|
||||
ciphertext[i] ^= 0xBB;
|
||||
expected_plaintext[i] ^= 0xBB;
|
||||
}
|
||||
let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext, true).unwrap();
|
||||
assert_eq!(decrypted, expected_plaintext);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chaining() {
|
||||
let mut env = TestEnv::default();
|
||||
let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]);
|
||||
let plaintext = vec![0xAA; 64];
|
||||
let ciphertext1 = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, true).unwrap();
|
||||
let ciphertext2 = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, true).unwrap();
|
||||
assert_eq!(ciphertext1.len(), 80);
|
||||
assert_eq!(ciphertext2.len(), 80);
|
||||
// The ciphertext should mutate in all blocks with a different IV.
|
||||
let block_iter1 = ciphertext1.chunks_exact(16);
|
||||
let block_iter2 = ciphertext2.chunks_exact(16);
|
||||
for (block1, block2) in block_iter1.zip(block_iter2) {
|
||||
assert_ne!(block1, block2);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_ecdsa_from_bytes() {
|
||||
let mut env = TestEnv::default();
|
||||
let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
|
||||
let key_bytes = private_key.to_bytes();
|
||||
assert_eq!(
|
||||
PrivateKey::new_ecdsa_from_bytes(&key_bytes),
|
||||
Some(private_key)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "ed25519")]
|
||||
fn test_new_ed25519_from_bytes() {
|
||||
let mut env = TestEnv::default();
|
||||
let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Eddsa);
|
||||
let key_bytes = private_key.to_bytes();
|
||||
assert_eq!(
|
||||
PrivateKey::new_ed25519_from_bytes(&key_bytes),
|
||||
Some(private_key)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_ecdsa_from_bytes_wrong_length() {
|
||||
assert_eq!(PrivateKey::new_ecdsa_from_bytes(&[0x55; 16]), None);
|
||||
assert_eq!(PrivateKey::new_ecdsa_from_bytes(&[0x55; 31]), None);
|
||||
assert_eq!(PrivateKey::new_ecdsa_from_bytes(&[0x55; 33]), None);
|
||||
assert_eq!(PrivateKey::new_ecdsa_from_bytes(&[0x55; 64]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "ed25519")]
|
||||
fn test_new_ed25519_from_bytes_wrong_length() {
|
||||
assert_eq!(PrivateKey::new_ed25519_from_bytes(&[0x55; 16]), None);
|
||||
assert_eq!(PrivateKey::new_ed25519_from_bytes(&[0x55; 31]), None);
|
||||
assert_eq!(PrivateKey::new_ed25519_from_bytes(&[0x55; 33]), None);
|
||||
assert_eq!(PrivateKey::new_ed25519_from_bytes(&[0x55; 64]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_private_key_get_pub_key() {
|
||||
let mut env = TestEnv::default();
|
||||
let private_key = PrivateKey::new_ecdsa(&mut env);
|
||||
let ecdsa_key = private_key.ecdsa_key(&mut env).unwrap();
|
||||
let public_key = ecdsa_key.genpk();
|
||||
assert_eq!(
|
||||
private_key.get_pub_key(&mut env),
|
||||
Ok(CoseKey::from(public_key))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_private_key_sign_and_encode() {
|
||||
let mut env = TestEnv::default();
|
||||
let message = [0x5A; 32];
|
||||
let private_key = PrivateKey::new_ecdsa(&mut env);
|
||||
let ecdsa_key = private_key.ecdsa_key(&mut env).unwrap();
|
||||
let signature = ecdsa_key.sign_rfc6979::<Sha256>(&message).to_asn1_der();
|
||||
assert_eq!(
|
||||
private_key.sign_and_encode(&mut env, &message),
|
||||
Ok(signature)
|
||||
);
|
||||
}
|
||||
|
||||
fn test_private_key_signature_algorithm(signature_algorithm: SignatureAlgorithm) {
|
||||
let mut env = TestEnv::default();
|
||||
let private_key = PrivateKey::new(&mut env, signature_algorithm);
|
||||
assert_eq!(private_key.signature_algorithm(), signature_algorithm);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ecdsa_private_key_signature_algorithm() {
|
||||
test_private_key_signature_algorithm(SignatureAlgorithm::Es256);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "ed25519")]
|
||||
fn test_ed25519_private_key_signature_algorithm() {
|
||||
test_private_key_signature_algorithm(SignatureAlgorithm::Eddsa);
|
||||
}
|
||||
|
||||
fn test_private_key_from_to_cbor(signature_algorithm: SignatureAlgorithm) {
|
||||
let mut env = TestEnv::default();
|
||||
let private_key = PrivateKey::new(&mut env, signature_algorithm);
|
||||
let cbor = cbor::Value::from(&private_key);
|
||||
assert_eq!(PrivateKey::try_from(cbor), Ok(private_key),);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ecdsa_private_key_from_to_cbor() {
|
||||
test_private_key_from_to_cbor(SignatureAlgorithm::Es256);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "ed25519")]
|
||||
fn test_ed25519_private_key_from_to_cbor() {
|
||||
test_private_key_from_to_cbor(SignatureAlgorithm::Eddsa);
|
||||
}
|
||||
|
||||
fn test_private_key_from_bad_cbor(signature_algorithm: SignatureAlgorithm) {
|
||||
let cbor = cbor_array![
|
||||
cbor_int!(signature_algorithm as i64),
|
||||
cbor_bytes!(vec![0x88; 32]),
|
||||
// The array is too long.
|
||||
cbor_int!(0),
|
||||
];
|
||||
assert_eq!(
|
||||
PrivateKey::try_from(cbor),
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ecdsa_private_key_from_bad_cbor() {
|
||||
test_private_key_from_bad_cbor(SignatureAlgorithm::Es256);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "ed25519")]
|
||||
fn test_ed25519_private_key_from_bad_cbor() {
|
||||
test_private_key_from_bad_cbor(SignatureAlgorithm::Eddsa);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_private_key_from_bad_cbor_unsupported_algo() {
|
||||
let cbor = cbor_array![
|
||||
// This algorithms doesn't exist.
|
||||
cbor_int!(-1),
|
||||
cbor_bytes!(vec![0x88; 32]),
|
||||
];
|
||||
assert_eq!(
|
||||
PrivateKey::try_from(cbor),
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR),
|
||||
);
|
||||
}
|
||||
}
|
||||
702
libraries/opensk/src/ctap/ctap1.rs
Normal file
702
libraries/opensk/src/ctap/ctap1.rs
Normal file
@@ -0,0 +1,702 @@
|
||||
// Copyright 2019-2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::apdu::{Apdu, ApduStatusCode};
|
||||
use super::credential_id::{decrypt_credential_id, encrypt_to_credential_id};
|
||||
use super::crypto_wrapper::PrivateKey;
|
||||
use super::CtapState;
|
||||
use crate::api::attestation_store::{self, Attestation, AttestationStore};
|
||||
use crate::env::Env;
|
||||
use alloc::vec::Vec;
|
||||
use arrayref::array_ref;
|
||||
use core::convert::TryFrom;
|
||||
|
||||
// For now, they're the same thing with apdu.rs containing the authoritative definition
|
||||
pub type Ctap1StatusCode = ApduStatusCode;
|
||||
|
||||
// The specification referenced in this file is at:
|
||||
// https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.pdf
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Ctap1Flags {
|
||||
CheckOnly = 0x07,
|
||||
EnforceUpAndSign = 0x03,
|
||||
DontEnforceUpAndSign = 0x08,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for Ctap1Flags {
|
||||
type Error = Ctap1StatusCode;
|
||||
|
||||
fn try_from(value: u8) -> Result<Ctap1Flags, Ctap1StatusCode> {
|
||||
match value {
|
||||
0x07 => Ok(Ctap1Flags::CheckOnly),
|
||||
0x03 => Ok(Ctap1Flags::EnforceUpAndSign),
|
||||
0x08 => Ok(Ctap1Flags::DontEnforceUpAndSign),
|
||||
_ => Err(Ctap1StatusCode::SW_WRONG_DATA),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Ctap1Flags> for u8 {
|
||||
fn from(flags: Ctap1Flags) -> u8 {
|
||||
flags as u8
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum U2fCommand {
|
||||
Register {
|
||||
challenge: [u8; 32],
|
||||
application: [u8; 32],
|
||||
},
|
||||
Authenticate {
|
||||
challenge: [u8; 32],
|
||||
application: [u8; 32],
|
||||
key_handle: Vec<u8>,
|
||||
flags: Ctap1Flags,
|
||||
},
|
||||
Version,
|
||||
VendorSpecific {
|
||||
payload: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for U2fCommand {
|
||||
type Error = Ctap1StatusCode;
|
||||
|
||||
fn try_from(message: &[u8]) -> Result<Self, Ctap1StatusCode> {
|
||||
let apdu: Apdu = match Apdu::try_from(message) {
|
||||
Ok(apdu) => apdu,
|
||||
Err(apdu_status_code) => return Err(apdu_status_code),
|
||||
};
|
||||
|
||||
let lc = apdu.lc as usize;
|
||||
|
||||
// ISO7816 APDU Header format. Each cell is 1 byte. Note that the CTAP flavor always
|
||||
// encodes the length on 3 bytes and doesn't use the field "Le" (Length Expected).
|
||||
// We keep the 2 byte of "Le" for the packet length in mind, but always ignore its value.
|
||||
// Lc is using big-endian encoding
|
||||
// +-----+-----+----+----+-----+-----+-----+
|
||||
// | CLA | INS | P1 | P2 | Lc1 | Lc2 | Lc3 |
|
||||
// +-----+-----+----+----+-----+-----+-----+
|
||||
if apdu.header.cla != Ctap1Command::CTAP1_CLA {
|
||||
return Err(Ctap1StatusCode::SW_CLA_INVALID);
|
||||
}
|
||||
|
||||
// Since there is always request data, the expected length is either omitted or
|
||||
// encoded in 2 bytes.
|
||||
if lc != apdu.data.len() && lc + 2 != apdu.data.len() {
|
||||
return Err(Ctap1StatusCode::SW_WRONG_LENGTH);
|
||||
}
|
||||
|
||||
match apdu.header.ins {
|
||||
// U2F raw message format specification, Section 4.1
|
||||
// +-----------------+-------------------+
|
||||
// + Challenge (32B) | Application (32B) |
|
||||
// +-----------------+-------------------+
|
||||
Ctap1Command::U2F_REGISTER => {
|
||||
if lc != 64 {
|
||||
return Err(Ctap1StatusCode::SW_WRONG_LENGTH);
|
||||
}
|
||||
Ok(Self::Register {
|
||||
challenge: *array_ref!(apdu.data, 0, 32),
|
||||
application: *array_ref!(apdu.data, 32, 32),
|
||||
})
|
||||
}
|
||||
|
||||
// U2F raw message format specification, Section 5.1
|
||||
// +-----------------+-------------------+---------------------+------------+
|
||||
// + Challenge (32B) | Application (32B) | key handle len (1B) | key handle |
|
||||
// +-----------------+-------------------+---------------------+------------+
|
||||
Ctap1Command::U2F_AUTHENTICATE => {
|
||||
if lc < 65 {
|
||||
return Err(Ctap1StatusCode::SW_WRONG_LENGTH);
|
||||
}
|
||||
let handle_length = apdu.data[64] as usize;
|
||||
if lc != 65 + handle_length {
|
||||
return Err(Ctap1StatusCode::SW_WRONG_LENGTH);
|
||||
}
|
||||
let flag = Ctap1Flags::try_from(apdu.header.p1)?;
|
||||
Ok(Self::Authenticate {
|
||||
challenge: *array_ref!(apdu.data, 0, 32),
|
||||
application: *array_ref!(apdu.data, 32, 32),
|
||||
key_handle: apdu.data[65..].to_vec(),
|
||||
flags: flag,
|
||||
})
|
||||
}
|
||||
|
||||
// U2F raw message format specification, Section 6.1
|
||||
Ctap1Command::U2F_VERSION => {
|
||||
if lc != 0 {
|
||||
return Err(Ctap1StatusCode::SW_WRONG_LENGTH);
|
||||
}
|
||||
Ok(Self::Version)
|
||||
}
|
||||
|
||||
// For Vendor specific command.
|
||||
Ctap1Command::VENDOR_SPECIFIC_FIRST..=Ctap1Command::VENDOR_SPECIFIC_LAST => {
|
||||
Ok(Self::VendorSpecific {
|
||||
payload: apdu.data.to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
_ => Err(Ctap1StatusCode::SW_INS_INVALID),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Ctap1Command {}
|
||||
|
||||
impl Ctap1Command {
|
||||
const CTAP1_CLA: u8 = 0;
|
||||
// This byte is used in Register, but only serves backwards compatibility.
|
||||
const LEGACY_BYTE: u8 = 0x05;
|
||||
// This byte is hardcoded into the specification of Authenticate.
|
||||
const USER_PRESENCE_INDICATOR_BYTE: u8 = 0x01;
|
||||
|
||||
// CTAP1/U2F commands
|
||||
// U2F raw message format specification 1.2 (version 20170411)
|
||||
const U2F_REGISTER: u8 = 0x01;
|
||||
const U2F_AUTHENTICATE: u8 = 0x02;
|
||||
const U2F_VERSION: u8 = 0x03;
|
||||
const VENDOR_SPECIFIC_FIRST: u8 = 0x40;
|
||||
const VENDOR_SPECIFIC_LAST: u8 = 0xBF;
|
||||
|
||||
pub fn process_command<E: Env>(
|
||||
env: &mut E,
|
||||
message: &[u8],
|
||||
ctap_state: &mut CtapState<E>,
|
||||
) -> Result<Vec<u8>, Ctap1StatusCode> {
|
||||
if !ctap_state
|
||||
.allows_ctap1(env)
|
||||
.map_err(|_| Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?
|
||||
{
|
||||
return Err(Ctap1StatusCode::SW_COMMAND_NOT_ALLOWED);
|
||||
}
|
||||
let command = U2fCommand::try_from(message)?;
|
||||
match command {
|
||||
U2fCommand::Register {
|
||||
challenge,
|
||||
application,
|
||||
} => {
|
||||
if !ctap_state.u2f_up_state.consume_up(env) {
|
||||
return Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED);
|
||||
}
|
||||
Ctap1Command::process_register(env, challenge, application)
|
||||
}
|
||||
|
||||
U2fCommand::Authenticate {
|
||||
challenge,
|
||||
application,
|
||||
key_handle,
|
||||
flags,
|
||||
} => {
|
||||
// The order is important due to side effects of checking user presence.
|
||||
if flags == Ctap1Flags::EnforceUpAndSign && !ctap_state.u2f_up_state.consume_up(env)
|
||||
{
|
||||
return Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED);
|
||||
}
|
||||
Ctap1Command::process_authenticate(
|
||||
env,
|
||||
challenge,
|
||||
application,
|
||||
key_handle,
|
||||
flags,
|
||||
ctap_state,
|
||||
)
|
||||
}
|
||||
|
||||
// U2F raw message format specification (version 20170411) section 6.3
|
||||
U2fCommand::Version => Ok(Vec::<u8>::from(super::U2F_VERSION_STRING)),
|
||||
|
||||
// TODO: should we return an error instead such as SW_INS_NOT_SUPPORTED?
|
||||
U2fCommand::VendorSpecific { .. } => Err(Ctap1StatusCode::SW_SUCCESS),
|
||||
}
|
||||
}
|
||||
|
||||
// U2F raw message format specification (version 20170411) section 4.3
|
||||
// In case of success we need to send back the following reply
|
||||
// (excluding ISO7816 success code)
|
||||
// +------+--------------------+---------------------+------------+------------+------+
|
||||
// + 0x05 | User pub key (65B) | key handle len (1B) | key handle | X.509 Cert | Sign |
|
||||
// +------+--------------------+---------------------+------------+------------+------+
|
||||
//
|
||||
// Where Sign is an ECDSA signature over the following structure:
|
||||
// +------+-------------------+-----------------+------------+--------------------+
|
||||
// + 0x00 | application (32B) | challenge (32B) | key handle | User pub key (65B) |
|
||||
// +------+-------------------+-----------------+------------+--------------------+
|
||||
fn process_register(
|
||||
env: &mut impl Env,
|
||||
challenge: [u8; 32],
|
||||
application: [u8; 32],
|
||||
) -> Result<Vec<u8>, Ctap1StatusCode> {
|
||||
let private_key = PrivateKey::new_ecdsa(env);
|
||||
let sk = private_key
|
||||
.ecdsa_key(env)
|
||||
.map_err(|_| Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?;
|
||||
let pk = sk.genpk();
|
||||
let key_handle = encrypt_to_credential_id(env, &private_key, &application, None, None)
|
||||
.map_err(|_| Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?;
|
||||
if key_handle.len() > 0xFF {
|
||||
// This is just being defensive with unreachable code.
|
||||
return Err(Ctap1StatusCode::SW_INTERNAL_EXCEPTION);
|
||||
}
|
||||
|
||||
let Attestation {
|
||||
private_key,
|
||||
certificate,
|
||||
} = env
|
||||
.attestation_store()
|
||||
.get(&attestation_store::Id::Batch)?
|
||||
.ok_or(Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?;
|
||||
|
||||
let mut response = Vec::with_capacity(105 + key_handle.len() + certificate.len());
|
||||
response.push(Ctap1Command::LEGACY_BYTE);
|
||||
let user_pk = pk.to_uncompressed();
|
||||
response.extend_from_slice(&user_pk);
|
||||
response.push(key_handle.len() as u8);
|
||||
response.extend(key_handle.clone());
|
||||
response.extend_from_slice(&certificate);
|
||||
|
||||
// The first byte is reserved.
|
||||
let mut signature_data = Vec::with_capacity(66 + key_handle.len());
|
||||
signature_data.push(0x00);
|
||||
signature_data.extend(&application);
|
||||
signature_data.extend(&challenge);
|
||||
signature_data.extend(key_handle);
|
||||
signature_data.extend_from_slice(&user_pk);
|
||||
|
||||
let attestation_key = crypto::ecdsa::SecKey::from_bytes(&private_key).unwrap();
|
||||
let signature = attestation_key.sign_rfc6979::<crypto::sha256::Sha256>(&signature_data);
|
||||
|
||||
response.extend(signature.to_asn1_der());
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
// U2F raw message format specification (version 20170411) section 5.4
|
||||
// In case of success we need to send back the following reply
|
||||
// (excluding ISO7816 success code)
|
||||
// +---------+--------------+-----------+
|
||||
// + UP (1B) | Counter (4B) | Signature |
|
||||
// +---------+--------------+-----------+
|
||||
// UP only has 2 defined values:
|
||||
// - 0x00: user presence was not verified
|
||||
// - 0x01: user presence was verified
|
||||
//
|
||||
// Where Signature is an ECDSA signature over the following structure:
|
||||
// +-------------------+---------+--------------+-----------------+
|
||||
// + application (32B) | UP (1B) | Counter (4B) | challenge (32B) |
|
||||
// +-------------------+---------+--------------+-----------------+
|
||||
fn process_authenticate<E: Env>(
|
||||
env: &mut E,
|
||||
challenge: [u8; 32],
|
||||
application: [u8; 32],
|
||||
key_handle: Vec<u8>,
|
||||
flags: Ctap1Flags,
|
||||
ctap_state: &mut CtapState<E>,
|
||||
) -> Result<Vec<u8>, Ctap1StatusCode> {
|
||||
let credential_source = decrypt_credential_id(env, key_handle, &application)
|
||||
.map_err(|_| Ctap1StatusCode::SW_WRONG_DATA)?;
|
||||
if let Some(credential_source) = credential_source {
|
||||
let ecdsa_key = credential_source
|
||||
.private_key
|
||||
.ecdsa_key(env)
|
||||
.map_err(|_| Ctap1StatusCode::SW_WRONG_DATA)?;
|
||||
if flags == Ctap1Flags::CheckOnly {
|
||||
return Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED);
|
||||
}
|
||||
ctap_state
|
||||
.increment_global_signature_counter(env)
|
||||
.map_err(|_| Ctap1StatusCode::SW_WRONG_DATA)?;
|
||||
let mut signature_data = ctap_state
|
||||
.generate_auth_data(
|
||||
env,
|
||||
&application,
|
||||
Ctap1Command::USER_PRESENCE_INDICATOR_BYTE,
|
||||
)
|
||||
.map_err(|_| Ctap1StatusCode::SW_WRONG_DATA)?;
|
||||
signature_data.extend(&challenge);
|
||||
let signature = ecdsa_key.sign_rfc6979::<crypto::sha256::Sha256>(&signature_data);
|
||||
|
||||
let mut response = signature_data[application.len()..application.len() + 5].to_vec();
|
||||
response.extend(signature.to_asn1_der());
|
||||
Ok(response)
|
||||
} else {
|
||||
Err(Ctap1StatusCode::SW_WRONG_DATA)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::super::credential_id::CBOR_CREDENTIAL_ID_SIZE;
|
||||
use super::super::data_formats::SignatureAlgorithm;
|
||||
use super::super::TOUCH_TIMEOUT_MS;
|
||||
use super::*;
|
||||
use crate::api::customization::Customization;
|
||||
use crate::ctap::storage;
|
||||
use crate::env::test::TestEnv;
|
||||
use crypto::Hash256;
|
||||
|
||||
fn create_register_message(application: &[u8; 32]) -> Vec<u8> {
|
||||
let mut message = vec![
|
||||
Ctap1Command::CTAP1_CLA,
|
||||
Ctap1Command::U2F_REGISTER,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x40,
|
||||
];
|
||||
let challenge = [0x0C; 32];
|
||||
message.extend(&challenge);
|
||||
message.extend(application);
|
||||
message
|
||||
}
|
||||
|
||||
fn create_authenticate_message(
|
||||
application: &[u8; 32],
|
||||
flags: Ctap1Flags,
|
||||
key_handle: &[u8],
|
||||
) -> Vec<u8> {
|
||||
let mut message = vec![
|
||||
Ctap1Command::CTAP1_CLA,
|
||||
Ctap1Command::U2F_AUTHENTICATE,
|
||||
flags.into(),
|
||||
0x00,
|
||||
0x00,
|
||||
];
|
||||
message.extend(&(65 + CBOR_CREDENTIAL_ID_SIZE as u16).to_be_bytes());
|
||||
let challenge = [0x0C; 32];
|
||||
message.extend(&challenge);
|
||||
message.extend(application);
|
||||
message.push(CBOR_CREDENTIAL_ID_SIZE as u8);
|
||||
message.extend(key_handle);
|
||||
message
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_allowed() {
|
||||
let mut env = TestEnv::default();
|
||||
env.user_presence()
|
||||
.set(|| panic!("Unexpected user presence check in CTAP1"));
|
||||
let mut ctap_state = CtapState::new(&mut env);
|
||||
storage::toggle_always_uv(&mut env).unwrap();
|
||||
|
||||
let application = [0x0A; 32];
|
||||
let message = create_register_message(&application);
|
||||
ctap_state.u2f_up_state.consume_up(&mut env);
|
||||
ctap_state.u2f_up_state.grant_up(&mut env);
|
||||
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
|
||||
assert_eq!(response, Err(Ctap1StatusCode::SW_COMMAND_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_register() {
|
||||
let mut env = TestEnv::default();
|
||||
env.user_presence()
|
||||
.set(|| panic!("Unexpected user presence check in CTAP1"));
|
||||
let mut ctap_state = CtapState::new(&mut env);
|
||||
|
||||
let application = [0x0A; 32];
|
||||
let message = create_register_message(&application);
|
||||
ctap_state.u2f_up_state.consume_up(&mut env);
|
||||
ctap_state.u2f_up_state.grant_up(&mut env);
|
||||
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
|
||||
// Certificate and private key are missing
|
||||
assert_eq!(response, Err(Ctap1StatusCode::SW_INTERNAL_EXCEPTION));
|
||||
|
||||
let attestation = Attestation {
|
||||
private_key: [0x41; 32],
|
||||
certificate: vec![0x99; 100],
|
||||
};
|
||||
env.attestation_store()
|
||||
.set(&attestation_store::Id::Batch, Some(&attestation))
|
||||
.unwrap();
|
||||
ctap_state.u2f_up_state.consume_up(&mut env);
|
||||
ctap_state.u2f_up_state.grant_up(&mut env);
|
||||
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state).unwrap();
|
||||
assert_eq!(response[0], Ctap1Command::LEGACY_BYTE);
|
||||
assert_eq!(response[66], CBOR_CREDENTIAL_ID_SIZE as u8);
|
||||
assert!(decrypt_credential_id(
|
||||
&mut env,
|
||||
response[67..67 + CBOR_CREDENTIAL_ID_SIZE].to_vec(),
|
||||
&application,
|
||||
)
|
||||
.unwrap()
|
||||
.is_some());
|
||||
const CERT_START: usize = 67 + CBOR_CREDENTIAL_ID_SIZE;
|
||||
assert_eq!(
|
||||
&response[CERT_START..][..attestation.certificate.len()],
|
||||
&attestation.certificate
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_register_bad_message() {
|
||||
let mut env = TestEnv::default();
|
||||
env.user_presence()
|
||||
.set(|| panic!("Unexpected user presence check in CTAP1"));
|
||||
let mut ctap_state = CtapState::new(&mut env);
|
||||
|
||||
let application = [0x0A; 32];
|
||||
let message = create_register_message(&application);
|
||||
let response =
|
||||
Ctap1Command::process_command(&mut env, &message[..message.len() - 1], &mut ctap_state);
|
||||
|
||||
assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_LENGTH));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_register_without_up() {
|
||||
let application = [0x0A; 32];
|
||||
let message = create_register_message(&application);
|
||||
|
||||
let mut env = TestEnv::default();
|
||||
env.user_presence()
|
||||
.set(|| panic!("Unexpected user presence check in CTAP1"));
|
||||
let mut ctap_state = CtapState::new(&mut env);
|
||||
|
||||
ctap_state.u2f_up_state.consume_up(&mut env);
|
||||
ctap_state.u2f_up_state.grant_up(&mut env);
|
||||
env.clock().advance(TOUCH_TIMEOUT_MS);
|
||||
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
|
||||
assert_eq!(response, Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_authenticate_check_only() {
|
||||
let mut env = TestEnv::default();
|
||||
env.user_presence()
|
||||
.set(|| panic!("Unexpected user presence check in CTAP1"));
|
||||
let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
|
||||
let mut ctap_state = CtapState::new(&mut env);
|
||||
|
||||
let rp_id = "example.com";
|
||||
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
|
||||
let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap();
|
||||
let message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle);
|
||||
|
||||
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
|
||||
assert_eq!(response, Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_authenticate_check_only_wrong_rp() {
|
||||
let mut env = TestEnv::default();
|
||||
env.user_presence()
|
||||
.set(|| panic!("Unexpected user presence check in CTAP1"));
|
||||
let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
|
||||
let mut ctap_state = CtapState::new(&mut env);
|
||||
|
||||
let rp_id = "example.com";
|
||||
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
|
||||
let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap();
|
||||
let application = [0x55; 32];
|
||||
let message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle);
|
||||
|
||||
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
|
||||
assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_DATA));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_authenticate_check_only_wrong_length() {
|
||||
let mut env = TestEnv::default();
|
||||
env.user_presence()
|
||||
.set(|| panic!("Unexpected user presence check in CTAP1"));
|
||||
let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
|
||||
let mut ctap_state = CtapState::new(&mut env);
|
||||
|
||||
let rp_id = "example.com";
|
||||
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
|
||||
let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap();
|
||||
let mut message = create_authenticate_message(
|
||||
&application,
|
||||
Ctap1Flags::DontEnforceUpAndSign,
|
||||
&key_handle,
|
||||
);
|
||||
|
||||
message.push(0x00);
|
||||
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
|
||||
assert!(response.is_ok());
|
||||
|
||||
message.push(0x00);
|
||||
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
|
||||
assert!(response.is_ok());
|
||||
|
||||
message.push(0x00);
|
||||
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
|
||||
assert!(response.is_ok());
|
||||
|
||||
message.push(0x00);
|
||||
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
|
||||
assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_LENGTH));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_authenticate_check_only_wrong_cla() {
|
||||
let mut env = TestEnv::default();
|
||||
env.user_presence()
|
||||
.set(|| panic!("Unexpected user presence check in CTAP1"));
|
||||
let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
|
||||
let mut ctap_state = CtapState::new(&mut env);
|
||||
|
||||
let rp_id = "example.com";
|
||||
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
|
||||
let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap();
|
||||
let mut message =
|
||||
create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle);
|
||||
message[0] = 0xEE;
|
||||
|
||||
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
|
||||
assert_eq!(response, Err(Ctap1StatusCode::SW_CLA_INVALID));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_authenticate_check_only_wrong_ins() {
|
||||
let mut env = TestEnv::default();
|
||||
env.user_presence()
|
||||
.set(|| panic!("Unexpected user presence check in CTAP1"));
|
||||
let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
|
||||
let mut ctap_state = CtapState::new(&mut env);
|
||||
|
||||
let rp_id = "example.com";
|
||||
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
|
||||
let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap();
|
||||
let mut message =
|
||||
create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle);
|
||||
message[1] = 0xEE;
|
||||
|
||||
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
|
||||
assert_eq!(response, Err(Ctap1StatusCode::SW_INS_INVALID));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_authenticate_check_only_wrong_flags() {
|
||||
let mut env = TestEnv::default();
|
||||
env.user_presence()
|
||||
.set(|| panic!("Unexpected user presence check in CTAP1"));
|
||||
let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
|
||||
let mut ctap_state = CtapState::new(&mut env);
|
||||
|
||||
let rp_id = "example.com";
|
||||
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
|
||||
let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap();
|
||||
let mut message =
|
||||
create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle);
|
||||
message[2] = 0xEE;
|
||||
|
||||
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
|
||||
assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_DATA));
|
||||
}
|
||||
|
||||
fn check_signature_counter(env: &mut impl Env, response: &[u8; 4], signature_counter: u32) {
|
||||
if env.customization().use_signature_counter() {
|
||||
assert_eq!(u32::from_be_bytes(*response), signature_counter);
|
||||
} else {
|
||||
assert_eq!(response, &[0x00, 0x00, 0x00, 0x00]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_authenticate_enforce() {
|
||||
let mut env = TestEnv::default();
|
||||
env.user_presence()
|
||||
.set(|| panic!("Unexpected user presence check in CTAP1"));
|
||||
let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
|
||||
let mut ctap_state = CtapState::new(&mut env);
|
||||
|
||||
let rp_id = "example.com";
|
||||
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
|
||||
let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap();
|
||||
let message =
|
||||
create_authenticate_message(&application, Ctap1Flags::EnforceUpAndSign, &key_handle);
|
||||
|
||||
ctap_state.u2f_up_state.consume_up(&mut env);
|
||||
ctap_state.u2f_up_state.grant_up(&mut env);
|
||||
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state).unwrap();
|
||||
assert_eq!(response[0], 0x01);
|
||||
let global_signature_counter = storage::global_signature_counter(&mut env).unwrap();
|
||||
check_signature_counter(
|
||||
&mut env,
|
||||
array_ref!(response, 1, 4),
|
||||
global_signature_counter,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_authenticate_dont_enforce() {
|
||||
let mut env = TestEnv::default();
|
||||
env.user_presence()
|
||||
.set(|| panic!("Unexpected user presence check in CTAP1"));
|
||||
let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
|
||||
let mut ctap_state = CtapState::new(&mut env);
|
||||
|
||||
let rp_id = "example.com";
|
||||
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
|
||||
let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap();
|
||||
let message = create_authenticate_message(
|
||||
&application,
|
||||
Ctap1Flags::DontEnforceUpAndSign,
|
||||
&key_handle,
|
||||
);
|
||||
|
||||
env.clock().advance(TOUCH_TIMEOUT_MS);
|
||||
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state).unwrap();
|
||||
assert_eq!(response[0], 0x01);
|
||||
let global_signature_counter = storage::global_signature_counter(&mut env).unwrap();
|
||||
check_signature_counter(
|
||||
&mut env,
|
||||
array_ref!(response, 1, 4),
|
||||
global_signature_counter,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_authenticate_bad_key_handle() {
|
||||
let application = [0x0A; 32];
|
||||
let key_handle = vec![0x00; CBOR_CREDENTIAL_ID_SIZE];
|
||||
let message =
|
||||
create_authenticate_message(&application, Ctap1Flags::EnforceUpAndSign, &key_handle);
|
||||
|
||||
let mut env = TestEnv::default();
|
||||
env.user_presence()
|
||||
.set(|| panic!("Unexpected user presence check in CTAP1"));
|
||||
let mut ctap_state = CtapState::new(&mut env);
|
||||
|
||||
ctap_state.u2f_up_state.consume_up(&mut env);
|
||||
ctap_state.u2f_up_state.grant_up(&mut env);
|
||||
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
|
||||
assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_DATA));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_authenticate_without_up() {
|
||||
let application = [0x0A; 32];
|
||||
let key_handle = vec![0x00; CBOR_CREDENTIAL_ID_SIZE];
|
||||
let message =
|
||||
create_authenticate_message(&application, Ctap1Flags::EnforceUpAndSign, &key_handle);
|
||||
|
||||
let mut env = TestEnv::default();
|
||||
env.user_presence()
|
||||
.set(|| panic!("Unexpected user presence check in CTAP1"));
|
||||
let mut ctap_state = CtapState::new(&mut env);
|
||||
|
||||
ctap_state.u2f_up_state.consume_up(&mut env);
|
||||
ctap_state.u2f_up_state.grant_up(&mut env);
|
||||
env.clock().advance(TOUCH_TIMEOUT_MS);
|
||||
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
|
||||
assert_eq!(response, Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED));
|
||||
}
|
||||
}
|
||||
2275
libraries/opensk/src/ctap/data_formats.rs
Normal file
2275
libraries/opensk/src/ctap/data_formats.rs
Normal file
File diff suppressed because it is too large
Load Diff
622
libraries/opensk/src/ctap/hid/mod.rs
Normal file
622
libraries/opensk/src/ctap/hid/mod.rs
Normal file
@@ -0,0 +1,622 @@
|
||||
// Copyright 2019-2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
mod receive;
|
||||
mod send;
|
||||
|
||||
// Implementation details must be public for testing (in particular fuzzing).
|
||||
#[cfg(feature = "std")]
|
||||
pub use self::receive::MessageAssembler;
|
||||
#[cfg(not(feature = "std"))]
|
||||
use self::receive::MessageAssembler;
|
||||
pub use self::send::HidPacketIterator;
|
||||
use super::status_code::Ctap2StatusCode;
|
||||
#[cfg(test)]
|
||||
use crate::env::test::TestEnv;
|
||||
use crate::env::Env;
|
||||
use alloc::vec;
|
||||
use alloc::vec::Vec;
|
||||
use arrayref::{array_ref, array_refs};
|
||||
#[cfg(test)]
|
||||
use enum_iterator::IntoEnumIterator;
|
||||
|
||||
// We implement CTAP 2.1 from 2021-06-15. Please see section
|
||||
// 11.2. USB Human Interface Device (USB HID)
|
||||
const CHANNEL_RESERVED: ChannelID = [0, 0, 0, 0];
|
||||
const CHANNEL_BROADCAST: ChannelID = [0xFF, 0xFF, 0xFF, 0xFF];
|
||||
const PACKET_TYPE_MASK: u8 = 0x80;
|
||||
|
||||
// See section 11.2.9.1.3. CTAPHID_INIT (0x06).
|
||||
const PROTOCOL_VERSION: u8 = 2;
|
||||
// The device version number is vendor-defined.
|
||||
const DEVICE_VERSION_MAJOR: u8 = 1;
|
||||
const DEVICE_VERSION_MINOR: u8 = 0;
|
||||
const DEVICE_VERSION_BUILD: u8 = 0;
|
||||
|
||||
pub type HidPacket = [u8; 64];
|
||||
pub type ChannelID = [u8; 4];
|
||||
|
||||
/// CTAPHID commands
|
||||
///
|
||||
/// See section 11.2.9. of FIDO 2.1 (2021-06-15).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(IntoEnumIterator))]
|
||||
pub enum CtapHidCommand {
|
||||
Ping = 0x01,
|
||||
Msg = 0x03,
|
||||
// Lock is optional and may be used in the future.
|
||||
Lock = 0x04,
|
||||
Init = 0x06,
|
||||
Wink = 0x08,
|
||||
Cbor = 0x10,
|
||||
Cancel = 0x11,
|
||||
Keepalive = 0x3B,
|
||||
Error = 0x3F,
|
||||
// The vendor range starts here, going from 0x40 to 0x7F.
|
||||
}
|
||||
|
||||
impl From<u8> for CtapHidCommand {
|
||||
fn from(cmd: u8) -> Self {
|
||||
match cmd {
|
||||
x if x == CtapHidCommand::Ping as u8 => CtapHidCommand::Ping,
|
||||
x if x == CtapHidCommand::Msg as u8 => CtapHidCommand::Msg,
|
||||
x if x == CtapHidCommand::Lock as u8 => CtapHidCommand::Lock,
|
||||
x if x == CtapHidCommand::Init as u8 => CtapHidCommand::Init,
|
||||
x if x == CtapHidCommand::Wink as u8 => CtapHidCommand::Wink,
|
||||
x if x == CtapHidCommand::Cbor as u8 => CtapHidCommand::Cbor,
|
||||
x if x == CtapHidCommand::Cancel as u8 => CtapHidCommand::Cancel,
|
||||
x if x == CtapHidCommand::Keepalive as u8 => CtapHidCommand::Keepalive,
|
||||
// This includes the actual error code 0x3F. Error is not used for incoming packets in
|
||||
// the specification, so we can safely reuse it for unknown bytes.
|
||||
_ => CtapHidCommand::Error,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// CTAPHID errors
|
||||
///
|
||||
/// See section 11.2.9.1.6. of FIDO 2.1 (2021-06-15).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum CtapHidError {
|
||||
/// The command in the request is invalid.
|
||||
InvalidCmd = 0x01,
|
||||
/// A parameter in the request is invalid.
|
||||
_InvalidPar = 0x02,
|
||||
/// The length of a message is too big.
|
||||
InvalidLen = 0x03,
|
||||
/// Expected a continuation packet with a specific sequence number, got another sequence number.
|
||||
///
|
||||
/// This error code is also used if we expect a continuation packet, and receive an init
|
||||
/// packet. We interpreted it as invalid seq number 0.
|
||||
InvalidSeq = 0x04,
|
||||
/// This packet arrived after a timeout.
|
||||
MsgTimeout = 0x05,
|
||||
/// A packet arrived on one channel while another is busy.
|
||||
ChannelBusy = 0x06,
|
||||
/// Command requires channel lock.
|
||||
_LockRequired = 0x0A,
|
||||
/// The requested channel ID is invalid.
|
||||
InvalidChannel = 0x0B,
|
||||
/// Unspecified error.
|
||||
_Other = 0x7F,
|
||||
/// This error is silently ignored.
|
||||
UnexpectedContinuation,
|
||||
}
|
||||
|
||||
/// Describes the structure of a parsed HID packet.
|
||||
///
|
||||
/// A packet is either an Init or a Continuation packet.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ProcessedPacket<'a> {
|
||||
InitPacket {
|
||||
cmd: u8,
|
||||
len: usize,
|
||||
data: &'a [u8; 57],
|
||||
},
|
||||
ContinuationPacket {
|
||||
seq: u8,
|
||||
data: &'a [u8; 59],
|
||||
},
|
||||
}
|
||||
|
||||
/// An assembled CTAPHID command.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Message {
|
||||
// Channel ID.
|
||||
pub cid: ChannelID,
|
||||
// Command.
|
||||
pub cmd: CtapHidCommand,
|
||||
// Bytes of the message.
|
||||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
/// A keepalive packet reports the reason why a command does not finish.
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum KeepaliveStatus {
|
||||
Processing = 0x01,
|
||||
UpNeeded = 0x02,
|
||||
}
|
||||
|
||||
/// Holds all state for receiving and sending HID packets.
|
||||
///
|
||||
/// This includes
|
||||
/// - state from not fully processed messages,
|
||||
/// - all allocated channels.
|
||||
///
|
||||
/// To process a packet and receive the response, call `parse_packet`. If you didn't receive any
|
||||
/// message or preprocessing discarded it, stop. Else process the message further, by handling the
|
||||
/// commands:
|
||||
///
|
||||
/// - PING (optional)
|
||||
/// - MSG
|
||||
/// - WINK
|
||||
/// - CBOR
|
||||
///
|
||||
/// To get packets to send from your processed message, call `split_message`. Summary:
|
||||
///
|
||||
/// 1. `HidPacket` -> `Option<Message>`
|
||||
/// 2. `Option<Message>` -> `Message`
|
||||
/// 3. `Message` -> `Message`
|
||||
/// 4. `Message` -> `HidPacketIterator`
|
||||
///
|
||||
/// These steps correspond to:
|
||||
///
|
||||
/// 1. `parse_packet` assembles the message and preprocesses all pure HID commands and errors.
|
||||
/// 2. If you didn't receive any message or preprocessing discarded it, stop.
|
||||
/// 3. Handles all CTAP protocol interactions.
|
||||
/// 4. `split_message` creates packets out of the response message.
|
||||
pub struct CtapHid<E: Env> {
|
||||
assembler: MessageAssembler<E>,
|
||||
// The specification only requires unique CIDs, the allocation algorithm is vendor specific.
|
||||
// We allocate them incrementally, that is all `cid` such that 1 <= cid <= allocated_cids are
|
||||
// allocated.
|
||||
// In packets, the ID encoding is Big Endian to match what is used throughout CTAP (with the
|
||||
// u32::to/from_be_bytes methods).
|
||||
// TODO(kaczmarczyck) We might want to limit or timeout open channels.
|
||||
allocated_cids: usize,
|
||||
capabilities: u8,
|
||||
}
|
||||
|
||||
impl<E: Env> CtapHid<E> {
|
||||
pub const CAPABILITY_WINK: u8 = 0x01;
|
||||
pub const CAPABILITY_CBOR: u8 = 0x04;
|
||||
#[cfg(any(not(feature = "with_ctap1"), feature = "vendor_hid"))]
|
||||
pub const CAPABILITY_NMSG: u8 = 0x08;
|
||||
|
||||
/// Creates a new CTAP HID packet parser.
|
||||
///
|
||||
/// The capabilities passed in are reported to the client in Init.
|
||||
pub fn new(capabilities: u8) -> CtapHid<E> {
|
||||
Self {
|
||||
assembler: MessageAssembler::default(),
|
||||
allocated_cids: 0,
|
||||
capabilities,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a packet, and preprocesses some messages and errors.
|
||||
///
|
||||
/// The preprocessed commands are:
|
||||
/// - INIT
|
||||
/// - CANCEL
|
||||
/// - ERROR
|
||||
/// - Unknown and unexpected commands like KEEPALIVE
|
||||
/// - LOCK is not implemented and currently treated like an unknown command
|
||||
///
|
||||
/// Commands that may still be processed:
|
||||
/// - PING
|
||||
/// - MSG
|
||||
/// - WINK
|
||||
/// - CBOR
|
||||
///
|
||||
/// You may ignore PING, it's behaving correctly by default (input == output).
|
||||
/// Ignoring the others is incorrect behavior. You have to at least replace them with an error
|
||||
/// message:
|
||||
/// `Self::error_message(message.cid, CtapHidError::InvalidCmd)`
|
||||
pub fn parse_packet(&mut self, env: &mut E, packet: &HidPacket) -> Option<Message> {
|
||||
match self.assembler.parse_packet(env, packet) {
|
||||
Ok(Some(message)) => {
|
||||
debug_ctap!(env, "Received message: {:02x?}", message);
|
||||
self.preprocess_message(message)
|
||||
}
|
||||
Ok(None) => {
|
||||
// Waiting for more packets to assemble the message, nothing to send for now.
|
||||
None
|
||||
}
|
||||
Err((cid, error)) => {
|
||||
if matches!(error, CtapHidError::UnexpectedContinuation) {
|
||||
None
|
||||
} else if !self.is_allocated_channel(cid) {
|
||||
Some(Self::error_message(cid, CtapHidError::InvalidChannel))
|
||||
} else {
|
||||
Some(Self::error_message(cid, error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes HID-only commands of a message and returns an outgoing message if necessary.
|
||||
///
|
||||
/// The preprocessed commands are:
|
||||
/// - INIT
|
||||
/// - CANCEL
|
||||
/// - ERROR
|
||||
/// - Unknown and unexpected commands like KEEPALIVE
|
||||
/// - LOCK is not implemented and currently treated like an unknown command
|
||||
fn preprocess_message(&mut self, message: Message) -> Option<Message> {
|
||||
let cid = message.cid;
|
||||
if !self.has_valid_channel(&message) {
|
||||
return Some(Self::error_message(cid, CtapHidError::InvalidChannel));
|
||||
}
|
||||
|
||||
match message.cmd {
|
||||
CtapHidCommand::Msg => Some(message),
|
||||
CtapHidCommand::Cbor => Some(message),
|
||||
// CTAP 2.1 from 2021-06-15, section 11.2.9.1.3.
|
||||
CtapHidCommand::Init => {
|
||||
if message.payload.len() != 8 {
|
||||
return Some(Self::error_message(cid, CtapHidError::InvalidLen));
|
||||
}
|
||||
|
||||
let new_cid = if cid == CHANNEL_BROADCAST {
|
||||
// TODO: Prevent allocating 2^32 channels.
|
||||
self.allocated_cids += 1;
|
||||
(self.allocated_cids as u32).to_be_bytes()
|
||||
} else {
|
||||
// Sync the channel and discard the current transaction.
|
||||
cid
|
||||
};
|
||||
|
||||
let mut payload = vec![0; 17];
|
||||
payload[..8].copy_from_slice(&message.payload);
|
||||
payload[8..12].copy_from_slice(&new_cid);
|
||||
payload[12] = PROTOCOL_VERSION;
|
||||
payload[13] = DEVICE_VERSION_MAJOR;
|
||||
payload[14] = DEVICE_VERSION_MINOR;
|
||||
payload[15] = DEVICE_VERSION_BUILD;
|
||||
payload[16] = self.capabilities;
|
||||
|
||||
Some(Message {
|
||||
cid,
|
||||
cmd: CtapHidCommand::Init,
|
||||
payload,
|
||||
})
|
||||
}
|
||||
// CTAP 2.1 from 2021-06-15, section 11.2.9.1.4.
|
||||
CtapHidCommand::Ping => {
|
||||
// Pong the same message.
|
||||
Some(message)
|
||||
}
|
||||
// CTAP 2.1 from 2021-06-15, section 11.2.9.1.5.
|
||||
CtapHidCommand::Cancel => {
|
||||
// Authenticators MUST NOT reply to this message.
|
||||
// CANCEL is handled during user presence checks in main.
|
||||
None
|
||||
}
|
||||
CtapHidCommand::Wink => Some(message),
|
||||
_ => {
|
||||
// Unknown or unsupported command.
|
||||
Some(Self::error_message(cid, CtapHidError::InvalidCmd))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn has_valid_channel(&self, message: &Message) -> bool {
|
||||
match message.cid {
|
||||
// Only INIT commands use the broadcast channel.
|
||||
CHANNEL_BROADCAST => message.cmd == CtapHidCommand::Init,
|
||||
// Check that the channel is allocated.
|
||||
_ => self.is_allocated_channel(message.cid),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_allocated_channel(&self, cid: ChannelID) -> bool {
|
||||
cid != CHANNEL_RESERVED && u32::from_be_bytes(cid) as usize <= self.allocated_cids
|
||||
}
|
||||
|
||||
pub fn error_message(cid: ChannelID, error_code: CtapHidError) -> Message {
|
||||
Message {
|
||||
cid,
|
||||
cmd: CtapHidCommand::Error,
|
||||
payload: vec![error_code as u8],
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to parse a raw packet.
|
||||
pub fn process_single_packet(packet: &HidPacket) -> (ChannelID, ProcessedPacket) {
|
||||
let (&cid, rest) = array_refs![packet, 4, 60];
|
||||
if rest[0] & PACKET_TYPE_MASK != 0 {
|
||||
let cmd = rest[0] & !PACKET_TYPE_MASK;
|
||||
let len = (rest[1] as usize) << 8 | (rest[2] as usize);
|
||||
(
|
||||
cid,
|
||||
ProcessedPacket::InitPacket {
|
||||
cmd,
|
||||
len,
|
||||
data: array_ref!(rest, 3, 57),
|
||||
},
|
||||
)
|
||||
} else {
|
||||
(
|
||||
cid,
|
||||
ProcessedPacket::ContinuationPacket {
|
||||
seq: rest[0],
|
||||
data: array_ref!(rest, 1, 59),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Splits the message and unwraps the result.
|
||||
///
|
||||
/// Unwrapping handles the case of payload lengths > 7609 bytes. All responses are fixed
|
||||
/// length, with the exception of:
|
||||
/// - PING, but here output equals the (validated) input,
|
||||
/// - CBOR, where long responses are conceivable.
|
||||
///
|
||||
/// Long CBOR responses should not happen, but we might not catch all edge cases, like for
|
||||
/// example long user names that are part of the output of an assertion. These cases should be
|
||||
/// correctly handled by the CTAP implementation. It is therefore an internal error from the
|
||||
/// HID perspective.
|
||||
pub fn split_message(message: Message) -> HidPacketIterator {
|
||||
let cid = message.cid;
|
||||
HidPacketIterator::new(message).unwrap_or_else(|| {
|
||||
// The error payload is 1 <= 7609 bytes, so unwrap() is safe.
|
||||
HidPacketIterator::new(Message {
|
||||
cid,
|
||||
cmd: CtapHidCommand::Cbor,
|
||||
payload: vec![Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR as u8],
|
||||
})
|
||||
.unwrap()
|
||||
})
|
||||
}
|
||||
|
||||
/// Generates the HID response packets for a keepalive status.
|
||||
pub fn keepalive(cid: ChannelID, status: KeepaliveStatus) -> HidPacketIterator {
|
||||
Self::split_message(Message {
|
||||
cid,
|
||||
cmd: CtapHidCommand::Keepalive,
|
||||
payload: vec![status as u8],
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn new_initialized() -> (Self, ChannelID) {
|
||||
(
|
||||
Self {
|
||||
assembler: MessageAssembler::default(),
|
||||
allocated_cids: 1,
|
||||
capabilities: 0x0D,
|
||||
},
|
||||
[0x00, 0x00, 0x00, 0x01],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_split_assemble() {
|
||||
let mut env = TestEnv::default();
|
||||
for payload_len in 0..7609 {
|
||||
let message = Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: CtapHidCommand::Cbor,
|
||||
payload: vec![0xFF; payload_len],
|
||||
};
|
||||
|
||||
let mut messages = Vec::new();
|
||||
let mut assembler = MessageAssembler::<TestEnv>::default();
|
||||
for packet in HidPacketIterator::new(message.clone()).unwrap() {
|
||||
match assembler.parse_packet(&mut env, &packet) {
|
||||
Ok(Some(msg)) => messages.push(msg),
|
||||
Ok(None) => (),
|
||||
Err(_) => panic!("Couldn't assemble packet: {:02x?}", &packet as &[u8]),
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(messages, vec![message]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_spurious_continuation_packet() {
|
||||
let mut env = TestEnv::default();
|
||||
let mut ctap_hid = CtapHid::<TestEnv>::new(0x0D);
|
||||
let mut packet = [0x00; 64];
|
||||
packet[0..7].copy_from_slice(&[0xC1, 0xC1, 0xC1, 0xC1, 0x00, 0x51, 0x51]);
|
||||
// Continuation packets are silently ignored.
|
||||
assert_eq!(ctap_hid.parse_packet(&mut env, &packet), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_init() {
|
||||
let mut ctap_hid = CtapHid::<TestEnv>::new(0x0D);
|
||||
let init_message = Message {
|
||||
cid: CHANNEL_BROADCAST,
|
||||
cmd: CtapHidCommand::Init,
|
||||
payload: vec![0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0],
|
||||
};
|
||||
let reply = ctap_hid.preprocess_message(init_message);
|
||||
assert_eq!(
|
||||
reply,
|
||||
Some(Message {
|
||||
cid: CHANNEL_BROADCAST,
|
||||
cmd: CtapHidCommand::Init,
|
||||
payload: vec![
|
||||
0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, // Nonce
|
||||
0x00, 0x00, 0x00, 0x01, // Allocated CID
|
||||
0x02, // Protocol version
|
||||
0x01, 0x00, 0x00, // Device version
|
||||
0x0D, // Capabilities
|
||||
]
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_init_for_sync() {
|
||||
let mut env = TestEnv::default();
|
||||
let (mut ctap_hid, cid) = CtapHid::<TestEnv>::new_initialized();
|
||||
|
||||
// Ping packet with a length longer than one packet.
|
||||
let mut packet1 = [0x51; 64];
|
||||
packet1[..4].copy_from_slice(&cid);
|
||||
packet1[4..7].copy_from_slice(&[0x81, 0x02, 0x00]);
|
||||
// Init packet on the same channel.
|
||||
let mut packet2 = [0x00; 64];
|
||||
packet2[..4].copy_from_slice(&cid);
|
||||
packet2[4..15].copy_from_slice(&[
|
||||
0x86, 0x00, 0x08, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0,
|
||||
]);
|
||||
assert_eq!(ctap_hid.parse_packet(&mut env, &packet1), None);
|
||||
assert_eq!(
|
||||
ctap_hid.parse_packet(&mut env, &packet2),
|
||||
Some(Message {
|
||||
cid,
|
||||
cmd: CtapHidCommand::Init,
|
||||
payload: vec![
|
||||
0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, // Nonce
|
||||
cid[0], cid[1], cid[2], cid[3], // Allocated CID
|
||||
0x02, // Protocol version
|
||||
0x01, 0x00, 0x00, // Device version
|
||||
0x0D, // Capabilities
|
||||
]
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_ping() {
|
||||
let mut env = TestEnv::default();
|
||||
let (mut ctap_hid, cid) = CtapHid::<TestEnv>::new_initialized();
|
||||
|
||||
let mut ping_packet = [0x00; 64];
|
||||
ping_packet[..4].copy_from_slice(&cid);
|
||||
ping_packet[4..9].copy_from_slice(&[0x81, 0x00, 0x02, 0x99, 0x99]);
|
||||
assert_eq!(
|
||||
ctap_hid.parse_packet(&mut env, &ping_packet),
|
||||
Some(Message {
|
||||
cid,
|
||||
cmd: CtapHidCommand::Ping,
|
||||
payload: vec![0x99, 0x99]
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_cancel() {
|
||||
let mut env = TestEnv::default();
|
||||
let (mut ctap_hid, cid) = CtapHid::<TestEnv>::new_initialized();
|
||||
|
||||
let mut cancel_packet = [0x00; 64];
|
||||
cancel_packet[..4].copy_from_slice(&cid);
|
||||
cancel_packet[4..7].copy_from_slice(&[0x91, 0x00, 0x00]);
|
||||
|
||||
let response = ctap_hid.parse_packet(&mut env, &cancel_packet);
|
||||
assert_eq!(response, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_message() {
|
||||
let message = Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: CtapHidCommand::Ping,
|
||||
payload: vec![0x99, 0x99],
|
||||
};
|
||||
let mut response = CtapHid::<TestEnv>::split_message(message);
|
||||
let mut expected_packet = [0x00; 64];
|
||||
expected_packet[..9]
|
||||
.copy_from_slice(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x02, 0x99, 0x99]);
|
||||
assert_eq!(response.next(), Some(expected_packet));
|
||||
assert_eq!(response.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_message_too_large() {
|
||||
let payload = vec![0xFF; 7609 + 1];
|
||||
let message = Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: CtapHidCommand::Cbor,
|
||||
payload,
|
||||
};
|
||||
let mut response = CtapHid::<TestEnv>::split_message(message);
|
||||
let mut expected_packet = [0x00; 64];
|
||||
expected_packet[..8].copy_from_slice(&[0x12, 0x34, 0x56, 0x78, 0x90, 0x00, 0x01, 0xF2]);
|
||||
assert_eq!(response.next(), Some(expected_packet));
|
||||
assert_eq!(response.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keepalive() {
|
||||
for &status in [KeepaliveStatus::Processing, KeepaliveStatus::UpNeeded].iter() {
|
||||
let cid = [0x12, 0x34, 0x56, 0x78];
|
||||
let mut response = CtapHid::<TestEnv>::keepalive(cid, status);
|
||||
let mut expected_packet = [0x00; 64];
|
||||
expected_packet[..8].copy_from_slice(&[
|
||||
0x12,
|
||||
0x34,
|
||||
0x56,
|
||||
0x78,
|
||||
0xBB,
|
||||
0x00,
|
||||
0x01,
|
||||
status as u8,
|
||||
]);
|
||||
assert_eq!(response.next(), Some(expected_packet));
|
||||
assert_eq!(response.next(), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_single_packet() {
|
||||
let cid = [0x12, 0x34, 0x56, 0x78];
|
||||
let mut packet = [0x00; 64];
|
||||
packet[..4].copy_from_slice(&cid);
|
||||
packet[4..9].copy_from_slice(&[0x81, 0x00, 0x02, 0x99, 0x99]);
|
||||
let (processed_cid, processed_packet) = CtapHid::<TestEnv>::process_single_packet(&packet);
|
||||
assert_eq!(processed_cid, cid);
|
||||
let expected_packet = ProcessedPacket::InitPacket {
|
||||
cmd: CtapHidCommand::Ping as u8,
|
||||
len: 2,
|
||||
data: array_ref!(packet, 7, 57),
|
||||
};
|
||||
assert_eq!(processed_packet, expected_packet);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_ctap_hid_command() {
|
||||
// 0x3E is unassigned.
|
||||
assert_eq!(CtapHidCommand::from(0x3E), CtapHidCommand::Error);
|
||||
for command in CtapHidCommand::into_enum_iter() {
|
||||
assert_eq!(CtapHidCommand::from(command as u8), command);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_message() {
|
||||
let cid = [0x12, 0x34, 0x56, 0x78];
|
||||
assert_eq!(
|
||||
CtapHid::<TestEnv>::error_message(cid, CtapHidError::InvalidCmd),
|
||||
Message {
|
||||
cid,
|
||||
cmd: CtapHidCommand::Error,
|
||||
payload: vec![0x01],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
594
libraries/opensk/src/ctap/hid/receive.rs
Normal file
594
libraries/opensk/src/ctap/hid/receive.rs
Normal file
@@ -0,0 +1,594 @@
|
||||
// Copyright 2019-2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::{
|
||||
ChannelID, CtapHid, CtapHidCommand, CtapHidError, HidPacket, Message, ProcessedPacket,
|
||||
};
|
||||
use crate::api::clock::Clock;
|
||||
use crate::api::customization::Customization;
|
||||
use crate::env::Env;
|
||||
use alloc::vec::Vec;
|
||||
use core::mem::swap;
|
||||
|
||||
// TODO: Is this timeout duration specified?
|
||||
const TIMEOUT_DURATION_MS: usize = 100;
|
||||
|
||||
/// A structure to assemble CTAPHID commands from a series of incoming USB HID packets.
|
||||
pub struct MessageAssembler<E: Env> {
|
||||
// Whether this is waiting to receive an initialization packet.
|
||||
idle: bool,
|
||||
// Current channel ID.
|
||||
cid: ChannelID,
|
||||
// Timestamp of the last packet received on the current channel.
|
||||
timer: <E::Clock as Clock>::Timer,
|
||||
// Current command.
|
||||
cmd: u8,
|
||||
// Sequence number expected for the next packet.
|
||||
seq: u8,
|
||||
// Number of bytes left to fill the current message.
|
||||
remaining_payload_len: usize,
|
||||
// Buffer for the current payload.
|
||||
payload: Vec<u8>,
|
||||
}
|
||||
|
||||
impl<E: Env> Default for MessageAssembler<E> {
|
||||
fn default() -> MessageAssembler<E> {
|
||||
MessageAssembler {
|
||||
idle: true,
|
||||
cid: [0, 0, 0, 0],
|
||||
timer: <E::Clock as Clock>::Timer::default(),
|
||||
cmd: 0,
|
||||
seq: 0,
|
||||
remaining_payload_len: 0,
|
||||
payload: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Env> MessageAssembler<E> {
|
||||
// Resets the message assembler to the idle state.
|
||||
// The caller can reset the assembler for example due to a timeout.
|
||||
fn reset(&mut self) {
|
||||
self.idle = true;
|
||||
self.cid = [0, 0, 0, 0];
|
||||
self.timer = <E::Clock as Clock>::Timer::default();
|
||||
self.cmd = 0;
|
||||
self.seq = 0;
|
||||
self.remaining_payload_len = 0;
|
||||
self.payload.clear();
|
||||
}
|
||||
|
||||
// Returns:
|
||||
// - An Ok() result if the packet was parsed correctly. This contains either Some(Vec<u8>) if a
|
||||
// full message was assembled after this packet, or None if more packets are needed to fill the
|
||||
// message.
|
||||
// - An Err() result if there was a parsing error.
|
||||
pub fn parse_packet(
|
||||
&mut self,
|
||||
env: &mut E,
|
||||
packet: &HidPacket,
|
||||
) -> Result<Option<Message>, (ChannelID, CtapHidError)> {
|
||||
// TODO: Support non-full-speed devices (i.e. packet len != 64)? This isn't recommended by
|
||||
// section 8.8.1
|
||||
let (cid, processed_packet) = CtapHid::<E>::process_single_packet(packet);
|
||||
|
||||
if !self.idle && env.clock().is_elapsed(&self.timer) {
|
||||
// The current channel timed out.
|
||||
// Save the channel ID and reset the state.
|
||||
let current_cid = self.cid;
|
||||
self.reset();
|
||||
|
||||
// If the packet is from the timed-out channel, send back a timeout error.
|
||||
// Otherwise, proceed with processing the packet.
|
||||
if cid == current_cid {
|
||||
return Err((cid, CtapHidError::MsgTimeout));
|
||||
}
|
||||
}
|
||||
|
||||
if self.idle {
|
||||
// Expecting an initialization packet.
|
||||
match processed_packet {
|
||||
ProcessedPacket::InitPacket { cmd, len, data } => {
|
||||
self.parse_init_packet(env, cid, cmd, len, data)
|
||||
}
|
||||
ProcessedPacket::ContinuationPacket { .. } => {
|
||||
// CTAP specification (version 20190130) section 8.1.5.4
|
||||
// Spurious continuation packets will be ignored.
|
||||
Err((cid, CtapHidError::UnexpectedContinuation))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Expecting a continuation packet from the current channel.
|
||||
|
||||
// CTAP specification (version 20190130) section 8.1.5.1
|
||||
// Reject packets from other channels.
|
||||
if cid != self.cid {
|
||||
return Err((cid, CtapHidError::ChannelBusy));
|
||||
}
|
||||
|
||||
match processed_packet {
|
||||
// Unexpected initialization packet.
|
||||
ProcessedPacket::InitPacket { cmd, len, data } => {
|
||||
self.reset();
|
||||
if cmd == CtapHidCommand::Init as u8 {
|
||||
self.parse_init_packet(env, cid, cmd, len, data)
|
||||
} else {
|
||||
Err((cid, CtapHidError::InvalidSeq))
|
||||
}
|
||||
}
|
||||
ProcessedPacket::ContinuationPacket { seq, data } => {
|
||||
if seq != self.seq {
|
||||
// Reject packets with the wrong sequence number.
|
||||
self.reset();
|
||||
Err((cid, CtapHidError::InvalidSeq))
|
||||
} else {
|
||||
// Update the last timestamp.
|
||||
self.timer = env.clock().make_timer(TIMEOUT_DURATION_MS);
|
||||
// Increment the sequence number for the next packet.
|
||||
self.seq += 1;
|
||||
Ok(self.append_payload(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_init_packet(
|
||||
&mut self,
|
||||
env: &mut E,
|
||||
cid: ChannelID,
|
||||
cmd: u8,
|
||||
len: usize,
|
||||
data: &[u8],
|
||||
) -> Result<Option<Message>, (ChannelID, CtapHidError)> {
|
||||
// Reject invalid lengths early to reduce the risk of running out of memory.
|
||||
// TODO: also reject invalid commands early?
|
||||
if len > env.customization().max_msg_size() {
|
||||
return Err((cid, CtapHidError::InvalidLen));
|
||||
}
|
||||
self.cid = cid;
|
||||
self.timer = env.clock().make_timer(TIMEOUT_DURATION_MS);
|
||||
self.cmd = cmd;
|
||||
self.seq = 0;
|
||||
self.remaining_payload_len = len;
|
||||
Ok(self.append_payload(data))
|
||||
}
|
||||
|
||||
fn append_payload(&mut self, data: &[u8]) -> Option<Message> {
|
||||
if data.len() < self.remaining_payload_len {
|
||||
self.payload.extend_from_slice(data);
|
||||
self.idle = false;
|
||||
self.remaining_payload_len -= data.len();
|
||||
None
|
||||
} else {
|
||||
self.payload
|
||||
.extend_from_slice(&data[..self.remaining_payload_len]);
|
||||
self.idle = true;
|
||||
let mut payload = Vec::new();
|
||||
swap(&mut self.payload, &mut payload);
|
||||
Some(Message {
|
||||
cid: self.cid,
|
||||
cmd: CtapHidCommand::from(self.cmd),
|
||||
payload,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::env::test::TestEnv;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn byte_extend(bytes: &[u8], padding: u8) -> HidPacket {
|
||||
let len = bytes.len();
|
||||
assert!(len <= 64);
|
||||
let mut result = [0; 64];
|
||||
result[..len].copy_from_slice(bytes);
|
||||
for byte in result[len..].iter_mut() {
|
||||
*byte = padding;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn zero_extend(bytes: &[u8]) -> HidPacket {
|
||||
byte_extend(bytes, 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_payload() {
|
||||
let mut env = TestEnv::default();
|
||||
let mut assembler = MessageAssembler::default();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x90])),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: CtapHidCommand::Cbor,
|
||||
payload: vec![]
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_one_packet() {
|
||||
let mut env = TestEnv::default();
|
||||
let mut assembler = MessageAssembler::default();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&mut env,
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x90, 0x00, 0x10]),
|
||||
),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: CtapHidCommand::Cbor,
|
||||
payload: vec![0x00; 0x10]
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nonzero_padding() {
|
||||
let mut env = TestEnv::default();
|
||||
// CTAP specification (version 20190130) section 8.1.4
|
||||
// It is written that "Unused bytes SHOULD be set to zero", so we test that non-zero
|
||||
// padding is accepted as well.
|
||||
let mut assembler = MessageAssembler::default();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&mut env,
|
||||
&byte_extend(&[0x12, 0x34, 0x56, 0x78, 0x90, 0x00, 0x10], 0xFF),
|
||||
),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: CtapHidCommand::Cbor,
|
||||
payload: vec![0xFF; 0x10]
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_two_packets() {
|
||||
let mut env = TestEnv::default();
|
||||
let mut assembler = MessageAssembler::default();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&mut env,
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x40]),
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x00])),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: CtapHidCommand::Ping,
|
||||
payload: vec![0x00; 0x40]
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_three_packets() {
|
||||
let mut env = TestEnv::default();
|
||||
let mut assembler = MessageAssembler::default();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&mut env,
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x80]),
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x00])),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x01])),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: CtapHidCommand::Ping,
|
||||
payload: vec![0x00; 0x80]
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_max_packets() {
|
||||
let mut env = TestEnv::default();
|
||||
let mut assembler = MessageAssembler::default();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&mut env,
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x1D, 0xB9]),
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
for seq in 0..0x7F {
|
||||
assert_eq!(
|
||||
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, seq])),
|
||||
Ok(None)
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x7F])),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: CtapHidCommand::Ping,
|
||||
payload: vec![0x00; 0x1DB9]
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_messages() {
|
||||
let mut env = TestEnv::default();
|
||||
// Check that after yielding a message, the assembler is ready to process new messages.
|
||||
let mut assembler = MessageAssembler::default();
|
||||
for i in 0..10 {
|
||||
// Introduce some variability in the messages.
|
||||
let cmd = CtapHidCommand::from(i + 1);
|
||||
let byte = 3 * i;
|
||||
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&mut env,
|
||||
&byte_extend(
|
||||
&[0x12, 0x34, 0x56, 0x78, 0x80 | cmd as u8, 0x00, 0x80],
|
||||
byte
|
||||
),
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&mut env,
|
||||
&byte_extend(&[0x12, 0x34, 0x56, 0x78, 0x00], byte),
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&mut env,
|
||||
&byte_extend(&[0x12, 0x34, 0x56, 0x78, 0x01], byte),
|
||||
),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd,
|
||||
payload: vec![byte; 0x80]
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_channel_switch() {
|
||||
let mut env = TestEnv::default();
|
||||
// Check that the assembler can process messages from multiple channels, sequentially.
|
||||
let mut assembler = MessageAssembler::default();
|
||||
for i in 0..10 {
|
||||
// Introduce some variability in the messages.
|
||||
let cid = 0x78 + i;
|
||||
let cmd = CtapHidCommand::from(i + 1);
|
||||
let byte = 3 * i;
|
||||
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&mut env,
|
||||
&byte_extend(&[0x12, 0x34, 0x56, cid, 0x80 | cmd as u8, 0x00, 0x80], byte),
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
assembler
|
||||
.parse_packet(&mut env, &byte_extend(&[0x12, 0x34, 0x56, cid, 0x00], byte)),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
assembler
|
||||
.parse_packet(&mut env, &byte_extend(&[0x12, 0x34, 0x56, cid, 0x01], byte)),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, cid],
|
||||
cmd,
|
||||
payload: vec![byte; 0x80]
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unexpected_channel() {
|
||||
let mut env = TestEnv::default();
|
||||
let mut assembler = MessageAssembler::default();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&mut env,
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x40]),
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
|
||||
// Check that many sorts of packets on another channel are ignored.
|
||||
for i in 0..=0xFF {
|
||||
let cmd = CtapHidCommand::from(i);
|
||||
for byte in 0..=0xFF {
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&mut env,
|
||||
&byte_extend(&[0x12, 0x34, 0x56, 0x9A, cmd as u8, 0x00], byte),
|
||||
),
|
||||
Err(([0x12, 0x34, 0x56, 0x9A], CtapHidError::ChannelBusy))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x00])),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: CtapHidCommand::Ping,
|
||||
payload: vec![0x00; 0x40]
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_spurious_continuation_packets() {
|
||||
let mut env = TestEnv::default();
|
||||
// CTAP specification (version 20190130) section 8.1.5.4
|
||||
// Spurious continuation packets appearing without a prior initialization packet will be
|
||||
// ignored.
|
||||
let mut assembler = MessageAssembler::default();
|
||||
for i in 0..0x80 {
|
||||
// Some legit packet.
|
||||
let byte = 2 * i;
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&mut env,
|
||||
&byte_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x10], byte),
|
||||
),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: CtapHidCommand::Ping,
|
||||
payload: vec![byte; 0x10]
|
||||
}))
|
||||
);
|
||||
|
||||
// Spurious continuation packet.
|
||||
let seq = i;
|
||||
assert_eq!(
|
||||
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, seq])),
|
||||
Err((
|
||||
[0x12, 0x34, 0x56, 0x78],
|
||||
CtapHidError::UnexpectedContinuation
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unexpected_init() {
|
||||
let mut env = TestEnv::default();
|
||||
let mut assembler = MessageAssembler::default();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&mut env,
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x40]),
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x80])),
|
||||
Err(([0x12, 0x34, 0x56, 0x78], CtapHidError::InvalidSeq))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unexpected_seq() {
|
||||
let mut env = TestEnv::default();
|
||||
let mut assembler = MessageAssembler::default();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&mut env,
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x40]),
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x01])),
|
||||
Err(([0x12, 0x34, 0x56, 0x78], CtapHidError::InvalidSeq))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timed_out_packet() {
|
||||
let mut env = TestEnv::default();
|
||||
let mut assembler = MessageAssembler::default();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&mut env,
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x40]),
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
env.clock().advance(TIMEOUT_DURATION_MS);
|
||||
assert_eq!(
|
||||
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x00])),
|
||||
Err(([0x12, 0x34, 0x56, 0x78], CtapHidError::MsgTimeout))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_just_in_time_packets() {
|
||||
let mut env = TestEnv::default();
|
||||
// Delay between each packet is just below the threshold.
|
||||
let delay = TIMEOUT_DURATION_MS - 1;
|
||||
|
||||
let mut assembler = MessageAssembler::default();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&mut env,
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x1D, 0xB9]),
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
for seq in 0..0x7F {
|
||||
env.clock().advance(delay);
|
||||
assert_eq!(
|
||||
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, seq])),
|
||||
Ok(None)
|
||||
);
|
||||
}
|
||||
env.clock().advance(delay);
|
||||
assert_eq!(
|
||||
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x7F])),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: CtapHidCommand::Ping,
|
||||
payload: vec![0x00; 0x1DB9]
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_init_sync() {
|
||||
let mut env = TestEnv::default();
|
||||
let mut assembler = MessageAssembler::default();
|
||||
// Ping packet with a length longer than one packet.
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&mut env,
|
||||
&byte_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x02, 0x00], 0x51),
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
// Init packet on the same channel.
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&mut env,
|
||||
&zero_extend(&[
|
||||
0x12, 0x34, 0x56, 0x78, 0x86, 0x00, 0x08, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC,
|
||||
0xDE, 0xF0
|
||||
]),
|
||||
),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: CtapHidCommand::Init,
|
||||
payload: vec![0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0]
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: more tests
|
||||
}
|
||||
316
libraries/opensk/src/ctap/hid/send.rs
Normal file
316
libraries/opensk/src/ctap/hid/send.rs
Normal file
@@ -0,0 +1,316 @@
|
||||
// Copyright 2019-2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::{HidPacket, Message};
|
||||
|
||||
const TYPE_INIT_BIT: u8 = 0x80;
|
||||
|
||||
/// Iterator for HID packets.
|
||||
///
|
||||
/// The `new` constructor splits the CTAP `Message` into `HidPacket`s for sending over USB.
|
||||
pub struct HidPacketIterator(Option<MessageSplitter>);
|
||||
|
||||
impl HidPacketIterator {
|
||||
pub fn new(message: Message) -> Option<HidPacketIterator> {
|
||||
let splitter = MessageSplitter::new(message);
|
||||
if splitter.is_some() {
|
||||
Some(HidPacketIterator(splitter))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn none() -> HidPacketIterator {
|
||||
HidPacketIterator(None)
|
||||
}
|
||||
|
||||
pub fn has_data(&self) -> bool {
|
||||
if let Some(ms) = &self.0 {
|
||||
ms.finished()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for HidPacketIterator {
|
||||
type Item = HidPacket;
|
||||
|
||||
fn next(&mut self) -> Option<HidPacket> {
|
||||
match &mut self.0 {
|
||||
Some(splitter) => splitter.next(),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MessageSplitter {
|
||||
message: Message,
|
||||
packet: HidPacket,
|
||||
seq: Option<u8>,
|
||||
i: usize,
|
||||
}
|
||||
|
||||
impl MessageSplitter {
|
||||
/// Try to split this message into an iterator of HID packets.
|
||||
///
|
||||
/// This fails if the message is too long to fit into a sequence of HID packets.
|
||||
pub fn new(message: Message) -> Option<MessageSplitter> {
|
||||
if message.payload.len() > 7609 {
|
||||
None
|
||||
} else {
|
||||
// Cache the CID, as it is constant for all packets in this message.
|
||||
let mut packet = [0; 64];
|
||||
packet[..4].copy_from_slice(&message.cid);
|
||||
|
||||
Some(MessageSplitter {
|
||||
message,
|
||||
packet,
|
||||
seq: None,
|
||||
i: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy as many bytes as possible from data to dst, and return how many bytes are copied.
|
||||
///
|
||||
/// Contrary to copy_from_slice, this doesn't require slices of the same length.
|
||||
/// All unused bytes in dst are set to zero, as if the data was padded with zeros to match.
|
||||
fn consume_data(dst: &mut [u8], data: &[u8]) -> usize {
|
||||
let dst_len = dst.len();
|
||||
let data_len = data.len();
|
||||
|
||||
if data_len <= dst_len {
|
||||
// data fits in dst, copy all the bytes.
|
||||
dst[..data_len].copy_from_slice(data);
|
||||
for byte in dst[data_len..].iter_mut() {
|
||||
*byte = 0;
|
||||
}
|
||||
data_len
|
||||
} else {
|
||||
// Fill all of dst.
|
||||
dst.copy_from_slice(&data[..dst_len]);
|
||||
dst_len
|
||||
}
|
||||
}
|
||||
|
||||
// Is there more data to iterate over?
|
||||
fn finished(&self) -> bool {
|
||||
let payload_len = self.message.payload.len();
|
||||
match self.seq {
|
||||
None => true,
|
||||
Some(_) => self.i < payload_len,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for MessageSplitter {
|
||||
type Item = HidPacket;
|
||||
|
||||
fn next(&mut self) -> Option<HidPacket> {
|
||||
let payload_len = self.message.payload.len();
|
||||
match self.seq {
|
||||
None => {
|
||||
// First, send an initialization packet.
|
||||
self.packet[4] = self.message.cmd as u8 | TYPE_INIT_BIT;
|
||||
self.packet[5] = (payload_len >> 8) as u8;
|
||||
self.packet[6] = payload_len as u8;
|
||||
|
||||
self.seq = Some(0);
|
||||
self.i =
|
||||
MessageSplitter::consume_data(&mut self.packet[7..], &self.message.payload);
|
||||
Some(self.packet)
|
||||
}
|
||||
Some(seq) => {
|
||||
// Send the next continuation packet, if any.
|
||||
if self.i < payload_len {
|
||||
self.packet[4] = seq;
|
||||
self.seq = Some(seq + 1);
|
||||
self.i += MessageSplitter::consume_data(
|
||||
&mut self.packet[5..],
|
||||
&self.message.payload[self.i..],
|
||||
);
|
||||
Some(self.packet)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::super::CtapHidCommand;
|
||||
use super::*;
|
||||
|
||||
fn assert_packet_output_equality(message: Message, expected_packets: Vec<HidPacket>) {
|
||||
let packets: Vec<HidPacket> = HidPacketIterator::new(message).unwrap().collect();
|
||||
assert_eq!(packets.len(), expected_packets.len());
|
||||
for (packet, expected_packet) in packets.iter().zip(expected_packets.iter()) {
|
||||
assert_eq!(packet as &[u8], expected_packet as &[u8]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hid_packet_iterator_single_packet() {
|
||||
let message = Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: CtapHidCommand::Cbor,
|
||||
payload: vec![0xAA, 0xBB],
|
||||
};
|
||||
let expected_packets: Vec<HidPacket> = vec![[
|
||||
0x12, 0x34, 0x56, 0x78, 0x90, 0x00, 0x02, 0xAA, 0xBB, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
]];
|
||||
assert_packet_output_equality(message, expected_packets);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hid_packet_iterator_big_single_packet() {
|
||||
let message = Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: CtapHidCommand::Cbor,
|
||||
payload: vec![0xAA; 64 - 7],
|
||||
};
|
||||
let expected_packets: Vec<HidPacket> = vec![[
|
||||
0x12, 0x34, 0x56, 0x78, 0x90, 0x00, 0x39, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
]];
|
||||
assert_packet_output_equality(message, expected_packets);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hid_packet_iterator_two_packets() {
|
||||
let message = Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: CtapHidCommand::Cbor,
|
||||
payload: vec![0xAA; 64 - 7 + 1],
|
||||
};
|
||||
let expected_packets: Vec<HidPacket> = vec![
|
||||
[
|
||||
0x12, 0x34, 0x56, 0x78, 0x90, 0x00, 0x3A, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
],
|
||||
[
|
||||
0x12, 0x34, 0x56, 0x78, 0x00, 0xAA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
],
|
||||
];
|
||||
assert_packet_output_equality(message, expected_packets);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hid_packet_iterator_two_full_packets() {
|
||||
let mut payload = vec![0xAA; 64 - 7];
|
||||
payload.extend(vec![0xBB; 64 - 5]);
|
||||
let message = Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: CtapHidCommand::Cbor,
|
||||
payload,
|
||||
};
|
||||
let expected_packets: Vec<HidPacket> = vec![
|
||||
[
|
||||
0x12, 0x34, 0x56, 0x78, 0x90, 0x00, 0x74, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
],
|
||||
[
|
||||
0x12, 0x34, 0x56, 0x78, 0x00, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB,
|
||||
0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB,
|
||||
0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB,
|
||||
0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB,
|
||||
0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB,
|
||||
],
|
||||
];
|
||||
assert_packet_output_equality(message, expected_packets);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hid_packet_iterator_max_packets() {
|
||||
let mut payload = vec![0xFF; 64 - 7];
|
||||
for i in 0..128 {
|
||||
payload.extend(vec![i + 1; 64 - 5]);
|
||||
}
|
||||
|
||||
// Sanity check for the length of the payload.
|
||||
assert_eq!(payload.len(), 0x1db9);
|
||||
|
||||
let message = Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: CtapHidCommand::Msg,
|
||||
payload,
|
||||
};
|
||||
|
||||
let mut expected_packets: Vec<HidPacket> = vec![[
|
||||
0x12, 0x34, 0x56, 0x78, 0x83, 0x1D, 0xB9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
]];
|
||||
for i in 0..128 {
|
||||
let mut packet: HidPacket = [0; 64];
|
||||
packet[0] = 0x12;
|
||||
packet[1] = 0x34;
|
||||
packet[2] = 0x56;
|
||||
packet[3] = 0x78;
|
||||
packet[4] = i;
|
||||
for byte in packet.iter_mut().skip(5) {
|
||||
*byte = i + 1;
|
||||
}
|
||||
expected_packets.push(packet);
|
||||
}
|
||||
|
||||
assert_packet_output_equality(message, expected_packets);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hid_packet_iterator_payload_one_too_large() {
|
||||
let payload = vec![0xFF; (64 - 7) + 128 * (64 - 5) + 1];
|
||||
assert_eq!(payload.len(), 0x1dba);
|
||||
let message = Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: CtapHidCommand::Msg,
|
||||
payload,
|
||||
};
|
||||
assert!(HidPacketIterator::new(message).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hid_packet_iterator_payload_way_too_large() {
|
||||
// Check that overflow of u16 doesn't bypass the size limit.
|
||||
let payload = vec![0xFF; 0x10000];
|
||||
let message = Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: CtapHidCommand::Msg,
|
||||
payload,
|
||||
};
|
||||
assert!(HidPacketIterator::new(message).is_none());
|
||||
}
|
||||
}
|
||||
459
libraries/opensk/src/ctap/large_blobs.rs
Normal file
459
libraries/opensk/src/ctap/large_blobs.rs
Normal file
@@ -0,0 +1,459 @@
|
||||
// Copyright 2020-2021 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::client_pin::{ClientPin, PinPermission};
|
||||
use super::command::AuthenticatorLargeBlobsParameters;
|
||||
use super::response::{AuthenticatorLargeBlobsResponse, ResponseData};
|
||||
use super::status_code::Ctap2StatusCode;
|
||||
use crate::api::customization::Customization;
|
||||
use crate::ctap::storage;
|
||||
use crate::env::Env;
|
||||
use alloc::vec;
|
||||
use alloc::vec::Vec;
|
||||
use byteorder::{ByteOrder, LittleEndian};
|
||||
use crypto::sha256::Sha256;
|
||||
use crypto::Hash256;
|
||||
|
||||
/// The length of the truncated hash that as appended to the large blob data.
|
||||
const TRUNCATED_HASH_LEN: usize = 16;
|
||||
|
||||
pub struct LargeBlobs {
|
||||
buffer: Vec<u8>,
|
||||
expected_length: usize,
|
||||
expected_next_offset: usize,
|
||||
}
|
||||
|
||||
/// Implements the logic for the AuthenticatorLargeBlobs command and keeps its state.
|
||||
impl LargeBlobs {
|
||||
pub fn new() -> LargeBlobs {
|
||||
LargeBlobs {
|
||||
buffer: Vec::new(),
|
||||
expected_length: 0,
|
||||
expected_next_offset: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Process the large blob command.
|
||||
pub fn process_command<E: Env>(
|
||||
&mut self,
|
||||
env: &mut E,
|
||||
client_pin: &mut ClientPin<E>,
|
||||
large_blobs_params: AuthenticatorLargeBlobsParameters,
|
||||
) -> Result<ResponseData, Ctap2StatusCode> {
|
||||
let AuthenticatorLargeBlobsParameters {
|
||||
get,
|
||||
set,
|
||||
offset,
|
||||
length,
|
||||
pin_uv_auth_param,
|
||||
pin_uv_auth_protocol,
|
||||
} = large_blobs_params;
|
||||
|
||||
let max_fragment_size = env.customization().max_msg_size() - 64;
|
||||
|
||||
if let Some(get) = get {
|
||||
if get > max_fragment_size || offset.checked_add(get).is_none() {
|
||||
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_LENGTH);
|
||||
}
|
||||
let config = storage::get_large_blob_array(env, offset, get)?;
|
||||
return Ok(ResponseData::AuthenticatorLargeBlobs(Some(
|
||||
AuthenticatorLargeBlobsResponse { config },
|
||||
)));
|
||||
}
|
||||
|
||||
if let Some(mut set) = set {
|
||||
if set.len() > max_fragment_size {
|
||||
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_LENGTH);
|
||||
}
|
||||
if offset == 0 {
|
||||
self.expected_length =
|
||||
length.ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)?;
|
||||
if self.expected_length > env.customization().max_large_blob_array_size() {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_LARGE_BLOB_STORAGE_FULL);
|
||||
}
|
||||
self.expected_next_offset = 0;
|
||||
}
|
||||
if offset != self.expected_next_offset {
|
||||
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_SEQ);
|
||||
}
|
||||
if storage::pin_hash(env)?.is_some() || storage::has_always_uv(env)? {
|
||||
let pin_uv_auth_param =
|
||||
pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?;
|
||||
let pin_uv_auth_protocol =
|
||||
pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?;
|
||||
let mut large_blob_data = vec![0xFF; 32];
|
||||
large_blob_data.extend(&[0x0C, 0x00]);
|
||||
let mut offset_bytes = [0u8; 4];
|
||||
LittleEndian::write_u32(&mut offset_bytes, offset as u32);
|
||||
large_blob_data.extend(&offset_bytes);
|
||||
large_blob_data.extend(&Sha256::hash(set.as_slice()));
|
||||
client_pin.verify_pin_uv_auth_token(
|
||||
&large_blob_data,
|
||||
&pin_uv_auth_param,
|
||||
pin_uv_auth_protocol,
|
||||
)?;
|
||||
client_pin.has_permission(PinPermission::LargeBlobWrite)?;
|
||||
}
|
||||
if offset + set.len() > self.expected_length {
|
||||
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
|
||||
}
|
||||
if offset == 0 {
|
||||
self.buffer = Vec::with_capacity(self.expected_length);
|
||||
}
|
||||
self.buffer.append(&mut set);
|
||||
self.expected_next_offset = self.buffer.len();
|
||||
if self.expected_next_offset == self.expected_length {
|
||||
self.expected_length = 0;
|
||||
self.expected_next_offset = 0;
|
||||
// Must be a positive number.
|
||||
let buffer_hash_index = self.buffer.len() - TRUNCATED_HASH_LEN;
|
||||
if Sha256::hash(&self.buffer[..buffer_hash_index])[..TRUNCATED_HASH_LEN]
|
||||
!= self.buffer[buffer_hash_index..]
|
||||
{
|
||||
self.buffer = Vec::new();
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE);
|
||||
}
|
||||
storage::commit_large_blob_array(env, &self.buffer)?;
|
||||
self.buffer = Vec::new();
|
||||
}
|
||||
return Ok(ResponseData::AuthenticatorLargeBlobs(None));
|
||||
}
|
||||
|
||||
// This should be unreachable, since the command has either get or set.
|
||||
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::super::data_formats::PinUvAuthProtocol;
|
||||
use super::super::pin_protocol::authenticate_pin_uv_auth_token;
|
||||
use super::*;
|
||||
use crate::env::test::TestEnv;
|
||||
|
||||
#[test]
|
||||
fn test_process_command_get_empty() {
|
||||
let mut env = TestEnv::default();
|
||||
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
|
||||
let pin_uv_auth_token = [0x55; 32];
|
||||
let mut client_pin = ClientPin::<TestEnv>::new_test(
|
||||
&mut env,
|
||||
key_agreement_key,
|
||||
pin_uv_auth_token,
|
||||
PinUvAuthProtocol::V1,
|
||||
);
|
||||
let mut large_blobs = LargeBlobs::new();
|
||||
|
||||
let large_blob = vec![
|
||||
0x80, 0x76, 0xBE, 0x8B, 0x52, 0x8D, 0x00, 0x75, 0xF7, 0xAA, 0xE9, 0x8D, 0x6F, 0xA5,
|
||||
0x7A, 0x6D, 0x3C,
|
||||
];
|
||||
let large_blobs_params = AuthenticatorLargeBlobsParameters {
|
||||
get: Some(large_blob.len()),
|
||||
set: None,
|
||||
offset: 0,
|
||||
length: None,
|
||||
pin_uv_auth_param: None,
|
||||
pin_uv_auth_protocol: None,
|
||||
};
|
||||
let large_blobs_response =
|
||||
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params);
|
||||
match large_blobs_response.unwrap() {
|
||||
ResponseData::AuthenticatorLargeBlobs(Some(response)) => {
|
||||
assert_eq!(response.config, large_blob);
|
||||
}
|
||||
_ => panic!("Invalid response type"),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_command_commit_and_get() {
|
||||
let mut env = TestEnv::default();
|
||||
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
|
||||
let pin_uv_auth_token = [0x55; 32];
|
||||
let mut client_pin = ClientPin::<TestEnv>::new_test(
|
||||
&mut env,
|
||||
key_agreement_key,
|
||||
pin_uv_auth_token,
|
||||
PinUvAuthProtocol::V1,
|
||||
);
|
||||
let mut large_blobs = LargeBlobs::new();
|
||||
|
||||
const BLOB_LEN: usize = 200;
|
||||
const DATA_LEN: usize = BLOB_LEN - TRUNCATED_HASH_LEN;
|
||||
let mut large_blob = vec![0x1B; DATA_LEN];
|
||||
large_blob.extend_from_slice(&Sha256::hash(&large_blob[..])[..TRUNCATED_HASH_LEN]);
|
||||
|
||||
let large_blobs_params = AuthenticatorLargeBlobsParameters {
|
||||
get: None,
|
||||
set: Some(large_blob[..BLOB_LEN / 2].to_vec()),
|
||||
offset: 0,
|
||||
length: Some(BLOB_LEN),
|
||||
pin_uv_auth_param: None,
|
||||
pin_uv_auth_protocol: None,
|
||||
};
|
||||
let large_blobs_response =
|
||||
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params);
|
||||
assert_eq!(
|
||||
large_blobs_response,
|
||||
Ok(ResponseData::AuthenticatorLargeBlobs(None))
|
||||
);
|
||||
|
||||
let large_blobs_params = AuthenticatorLargeBlobsParameters {
|
||||
get: None,
|
||||
set: Some(large_blob[BLOB_LEN / 2..].to_vec()),
|
||||
offset: BLOB_LEN / 2,
|
||||
length: None,
|
||||
pin_uv_auth_param: None,
|
||||
pin_uv_auth_protocol: None,
|
||||
};
|
||||
let large_blobs_response =
|
||||
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params);
|
||||
assert_eq!(
|
||||
large_blobs_response,
|
||||
Ok(ResponseData::AuthenticatorLargeBlobs(None))
|
||||
);
|
||||
|
||||
let large_blobs_params = AuthenticatorLargeBlobsParameters {
|
||||
get: Some(BLOB_LEN),
|
||||
set: None,
|
||||
offset: 0,
|
||||
length: None,
|
||||
pin_uv_auth_param: None,
|
||||
pin_uv_auth_protocol: None,
|
||||
};
|
||||
let large_blobs_response =
|
||||
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params);
|
||||
match large_blobs_response.unwrap() {
|
||||
ResponseData::AuthenticatorLargeBlobs(Some(response)) => {
|
||||
assert_eq!(response.config, large_blob);
|
||||
}
|
||||
_ => panic!("Invalid response type"),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_command_commit_unexpected_offset() {
|
||||
let mut env = TestEnv::default();
|
||||
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
|
||||
let pin_uv_auth_token = [0x55; 32];
|
||||
let mut client_pin = ClientPin::<TestEnv>::new_test(
|
||||
&mut env,
|
||||
key_agreement_key,
|
||||
pin_uv_auth_token,
|
||||
PinUvAuthProtocol::V1,
|
||||
);
|
||||
let mut large_blobs = LargeBlobs::new();
|
||||
|
||||
const BLOB_LEN: usize = 200;
|
||||
const DATA_LEN: usize = BLOB_LEN - TRUNCATED_HASH_LEN;
|
||||
let mut large_blob = vec![0x1B; DATA_LEN];
|
||||
large_blob.extend_from_slice(&Sha256::hash(&large_blob[..])[..TRUNCATED_HASH_LEN]);
|
||||
|
||||
let large_blobs_params = AuthenticatorLargeBlobsParameters {
|
||||
get: None,
|
||||
set: Some(large_blob[..BLOB_LEN / 2].to_vec()),
|
||||
offset: 0,
|
||||
length: Some(BLOB_LEN),
|
||||
pin_uv_auth_param: None,
|
||||
pin_uv_auth_protocol: None,
|
||||
};
|
||||
let large_blobs_response =
|
||||
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params);
|
||||
assert_eq!(
|
||||
large_blobs_response,
|
||||
Ok(ResponseData::AuthenticatorLargeBlobs(None))
|
||||
);
|
||||
|
||||
let large_blobs_params = AuthenticatorLargeBlobsParameters {
|
||||
get: None,
|
||||
set: Some(large_blob[BLOB_LEN / 2..].to_vec()),
|
||||
// The offset is 1 too big.
|
||||
offset: BLOB_LEN / 2 + 1,
|
||||
length: None,
|
||||
pin_uv_auth_param: None,
|
||||
pin_uv_auth_protocol: None,
|
||||
};
|
||||
let large_blobs_response =
|
||||
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params);
|
||||
assert_eq!(
|
||||
large_blobs_response,
|
||||
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_SEQ),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_command_commit_unexpected_length() {
|
||||
let mut env = TestEnv::default();
|
||||
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
|
||||
let pin_uv_auth_token = [0x55; 32];
|
||||
let mut client_pin = ClientPin::<TestEnv>::new_test(
|
||||
&mut env,
|
||||
key_agreement_key,
|
||||
pin_uv_auth_token,
|
||||
PinUvAuthProtocol::V1,
|
||||
);
|
||||
let mut large_blobs = LargeBlobs::new();
|
||||
|
||||
const BLOB_LEN: usize = 200;
|
||||
const DATA_LEN: usize = BLOB_LEN - TRUNCATED_HASH_LEN;
|
||||
let mut large_blob = vec![0x1B; DATA_LEN];
|
||||
large_blob.extend_from_slice(&Sha256::hash(&large_blob[..])[..TRUNCATED_HASH_LEN]);
|
||||
|
||||
let large_blobs_params = AuthenticatorLargeBlobsParameters {
|
||||
get: None,
|
||||
set: Some(large_blob[..BLOB_LEN / 2].to_vec()),
|
||||
offset: 0,
|
||||
// The length is 1 too small.
|
||||
length: Some(BLOB_LEN - 1),
|
||||
pin_uv_auth_param: None,
|
||||
pin_uv_auth_protocol: None,
|
||||
};
|
||||
let large_blobs_response =
|
||||
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params);
|
||||
assert_eq!(
|
||||
large_blobs_response,
|
||||
Ok(ResponseData::AuthenticatorLargeBlobs(None))
|
||||
);
|
||||
|
||||
let large_blobs_params = AuthenticatorLargeBlobsParameters {
|
||||
get: None,
|
||||
set: Some(large_blob[BLOB_LEN / 2..].to_vec()),
|
||||
offset: BLOB_LEN / 2,
|
||||
length: None,
|
||||
pin_uv_auth_param: None,
|
||||
pin_uv_auth_protocol: None,
|
||||
};
|
||||
let large_blobs_response =
|
||||
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params);
|
||||
assert_eq!(
|
||||
large_blobs_response,
|
||||
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_command_commit_end_offset_overflow() {
|
||||
let mut env = TestEnv::default();
|
||||
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
|
||||
let pin_uv_auth_token = [0x55; 32];
|
||||
let mut client_pin = ClientPin::<TestEnv>::new_test(
|
||||
&mut env,
|
||||
key_agreement_key,
|
||||
pin_uv_auth_token,
|
||||
PinUvAuthProtocol::V1,
|
||||
);
|
||||
let mut large_blobs = LargeBlobs::new();
|
||||
|
||||
let large_blobs_params = AuthenticatorLargeBlobsParameters {
|
||||
get: Some(1),
|
||||
set: None,
|
||||
offset: usize::MAX,
|
||||
length: None,
|
||||
pin_uv_auth_param: None,
|
||||
pin_uv_auth_protocol: None,
|
||||
};
|
||||
assert_eq!(
|
||||
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params),
|
||||
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_LENGTH),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_command_commit_unexpected_hash() {
|
||||
let mut env = TestEnv::default();
|
||||
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
|
||||
let pin_uv_auth_token = [0x55; 32];
|
||||
let mut client_pin = ClientPin::<TestEnv>::new_test(
|
||||
&mut env,
|
||||
key_agreement_key,
|
||||
pin_uv_auth_token,
|
||||
PinUvAuthProtocol::V1,
|
||||
);
|
||||
let mut large_blobs = LargeBlobs::new();
|
||||
|
||||
const BLOB_LEN: usize = 20;
|
||||
// This blob does not have an appropriate hash.
|
||||
let large_blob = vec![0x1B; BLOB_LEN];
|
||||
|
||||
let large_blobs_params = AuthenticatorLargeBlobsParameters {
|
||||
get: None,
|
||||
set: Some(large_blob.to_vec()),
|
||||
offset: 0,
|
||||
length: Some(BLOB_LEN),
|
||||
pin_uv_auth_param: None,
|
||||
pin_uv_auth_protocol: None,
|
||||
};
|
||||
let large_blobs_response =
|
||||
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params);
|
||||
assert_eq!(
|
||||
large_blobs_response,
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE),
|
||||
);
|
||||
}
|
||||
|
||||
fn test_helper_process_command_commit_with_pin(pin_uv_auth_protocol: PinUvAuthProtocol) {
|
||||
let mut env = TestEnv::default();
|
||||
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
|
||||
let pin_uv_auth_token = [0x55; 32];
|
||||
let mut client_pin = ClientPin::<TestEnv>::new_test(
|
||||
&mut env,
|
||||
key_agreement_key,
|
||||
pin_uv_auth_token,
|
||||
pin_uv_auth_protocol,
|
||||
);
|
||||
let mut large_blobs = LargeBlobs::new();
|
||||
|
||||
const BLOB_LEN: usize = 20;
|
||||
const DATA_LEN: usize = BLOB_LEN - TRUNCATED_HASH_LEN;
|
||||
let mut large_blob = vec![0x1B; DATA_LEN];
|
||||
large_blob.extend_from_slice(&Sha256::hash(&large_blob[..])[..TRUNCATED_HASH_LEN]);
|
||||
|
||||
storage::set_pin(&mut env, &[0u8; 16], 4).unwrap();
|
||||
let mut large_blob_data = vec![0xFF; 32];
|
||||
// Command constant and offset bytes.
|
||||
large_blob_data.extend(&[0x0C, 0x00, 0x00, 0x00, 0x00, 0x00]);
|
||||
large_blob_data.extend(&Sha256::hash(&large_blob));
|
||||
let pin_uv_auth_param = authenticate_pin_uv_auth_token(
|
||||
&pin_uv_auth_token,
|
||||
&large_blob_data,
|
||||
pin_uv_auth_protocol,
|
||||
);
|
||||
|
||||
let large_blobs_params = AuthenticatorLargeBlobsParameters {
|
||||
get: None,
|
||||
set: Some(large_blob),
|
||||
offset: 0,
|
||||
length: Some(BLOB_LEN),
|
||||
pin_uv_auth_param: Some(pin_uv_auth_param),
|
||||
pin_uv_auth_protocol: Some(pin_uv_auth_protocol),
|
||||
};
|
||||
let large_blobs_response =
|
||||
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params);
|
||||
assert_eq!(
|
||||
large_blobs_response,
|
||||
Ok(ResponseData::AuthenticatorLargeBlobs(None))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_command_commit_with_pin_v1() {
|
||||
test_helper_process_command_commit_with_pin(PinUvAuthProtocol::V1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_command_commit_with_pin_v2() {
|
||||
test_helper_process_command_commit_with_pin(PinUvAuthProtocol::V2);
|
||||
}
|
||||
}
|
||||
215
libraries/opensk/src/ctap/main_hid.rs
Normal file
215
libraries/opensk/src/ctap/main_hid.rs
Normal file
@@ -0,0 +1,215 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::api::clock::Clock;
|
||||
#[cfg(feature = "with_ctap1")]
|
||||
use crate::ctap::ctap1;
|
||||
#[cfg(feature = "with_ctap1")]
|
||||
use crate::ctap::hid::ChannelID;
|
||||
use crate::ctap::hid::{
|
||||
CtapHid, CtapHidCommand, CtapHidError, HidPacket, HidPacketIterator, Message,
|
||||
};
|
||||
use crate::ctap::{Channel, CtapState};
|
||||
use crate::env::Env;
|
||||
|
||||
const WINK_TIMEOUT_DURATION_MS: usize = 5000;
|
||||
|
||||
/// Implements the standard CTAP command processing for HID.
|
||||
pub struct MainHid<E: Env> {
|
||||
hid: CtapHid<E>,
|
||||
wink_permission: <E::Clock as Clock>::Timer,
|
||||
}
|
||||
|
||||
impl<E: Env> Default for MainHid<E> {
|
||||
/// Instantiates a HID handler for CTAP1, CTAP2 and Wink.
|
||||
fn default() -> Self {
|
||||
#[cfg(feature = "with_ctap1")]
|
||||
let capabilities = CtapHid::<E>::CAPABILITY_WINK | CtapHid::<E>::CAPABILITY_CBOR;
|
||||
#[cfg(not(feature = "with_ctap1"))]
|
||||
let capabilities = CtapHid::<E>::CAPABILITY_WINK
|
||||
| CtapHid::<E>::CAPABILITY_CBOR
|
||||
| CtapHid::<E>::CAPABILITY_NMSG;
|
||||
|
||||
let hid = CtapHid::new(capabilities);
|
||||
let wink_permission = <E::Clock as Clock>::Timer::default();
|
||||
MainHid {
|
||||
hid,
|
||||
wink_permission,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Env> MainHid<E> {
|
||||
/// Processes an incoming USB HID packet, and returns an iterator for all outgoing packets.
|
||||
pub fn process_hid_packet(
|
||||
&mut self,
|
||||
env: &mut E,
|
||||
packet: &HidPacket,
|
||||
ctap_state: &mut CtapState<E>,
|
||||
) -> HidPacketIterator {
|
||||
if let Some(message) = self.hid.parse_packet(env, packet) {
|
||||
let processed_message = self.process_message(env, message, ctap_state);
|
||||
debug_ctap!(env, "Sending message: {:02x?}", processed_message);
|
||||
CtapHid::<E>::split_message(processed_message)
|
||||
} else {
|
||||
HidPacketIterator::none()
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes a message's commands that affect the protocol outside HID.
|
||||
pub fn process_message(
|
||||
&mut self,
|
||||
env: &mut E,
|
||||
message: Message,
|
||||
ctap_state: &mut CtapState<E>,
|
||||
) -> Message {
|
||||
// If another command arrives, stop winking to prevent accidential button touches.
|
||||
self.wink_permission = <E::Clock as Clock>::Timer::default();
|
||||
|
||||
let cid = message.cid;
|
||||
match message.cmd {
|
||||
// CTAP 2.1 from 2021-06-15, section 11.2.9.1.1.
|
||||
CtapHidCommand::Msg => {
|
||||
// If we don't have CTAP1 backward compatibilty, this command is invalid.
|
||||
#[cfg(not(feature = "with_ctap1"))]
|
||||
return CtapHid::<E>::error_message(cid, CtapHidError::InvalidCmd);
|
||||
|
||||
#[cfg(feature = "with_ctap1")]
|
||||
match ctap1::Ctap1Command::process_command(env, &message.payload, ctap_state) {
|
||||
Ok(payload) => Self::ctap1_success_message(cid, &payload),
|
||||
Err(ctap1_status_code) => Self::ctap1_error_message(cid, ctap1_status_code),
|
||||
}
|
||||
}
|
||||
// CTAP 2.1 from 2021-06-15, section 11.2.9.1.2.
|
||||
CtapHidCommand::Cbor => {
|
||||
// Each transaction is atomic, so we process the command directly here and
|
||||
// don't handle any other packet in the meantime.
|
||||
// TODO: Send "Processing" type keep-alive packets in the meantime.
|
||||
let response =
|
||||
ctap_state.process_command(env, &message.payload, Channel::MainHid(cid));
|
||||
Message {
|
||||
cid,
|
||||
cmd: CtapHidCommand::Cbor,
|
||||
payload: response,
|
||||
}
|
||||
}
|
||||
// CTAP 2.1 from 2021-06-15, section 11.2.9.2.1.
|
||||
CtapHidCommand::Wink => {
|
||||
if message.payload.is_empty() {
|
||||
self.wink_permission = env.clock().make_timer(WINK_TIMEOUT_DURATION_MS);
|
||||
// The response is empty like the request.
|
||||
message
|
||||
} else {
|
||||
CtapHid::<E>::error_message(cid, CtapHidError::InvalidLen)
|
||||
}
|
||||
}
|
||||
// All other commands have already been processed, keep them as is.
|
||||
_ => message,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether a wink permission is currently granted.
|
||||
pub fn should_wink(&self, env: &mut E) -> bool {
|
||||
!env.clock().is_elapsed(&self.wink_permission)
|
||||
}
|
||||
|
||||
#[cfg(feature = "with_ctap1")]
|
||||
fn ctap1_error_message(cid: ChannelID, error_code: ctap1::Ctap1StatusCode) -> Message {
|
||||
let code: u16 = error_code.into();
|
||||
Message {
|
||||
cid,
|
||||
cmd: CtapHidCommand::Msg,
|
||||
payload: code.to_be_bytes().to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "with_ctap1")]
|
||||
fn ctap1_success_message(cid: ChannelID, payload: &[u8]) -> Message {
|
||||
let mut response = payload.to_vec();
|
||||
let code: u16 = ctap1::Ctap1StatusCode::SW_SUCCESS.into();
|
||||
response.extend_from_slice(&code.to_be_bytes());
|
||||
Message {
|
||||
cid,
|
||||
cmd: CtapHidCommand::Msg,
|
||||
payload: response,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::ctap::hid::ChannelID;
|
||||
use crate::env::test::TestEnv;
|
||||
|
||||
fn new_initialized() -> (MainHid<TestEnv>, ChannelID) {
|
||||
let (hid, cid) = CtapHid::new_initialized();
|
||||
let wink_permission = <<TestEnv as Env>::Clock as Clock>::Timer::default();
|
||||
(
|
||||
MainHid::<TestEnv> {
|
||||
hid,
|
||||
wink_permission,
|
||||
},
|
||||
cid,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_hid_packet() {
|
||||
let mut env = TestEnv::default();
|
||||
let mut ctap_state = CtapState::<TestEnv>::new(&mut env);
|
||||
let (mut main_hid, cid) = new_initialized();
|
||||
|
||||
let mut ping_packet = [0x00; 64];
|
||||
ping_packet[..4].copy_from_slice(&cid);
|
||||
ping_packet[4..9].copy_from_slice(&[0x81, 0x00, 0x02, 0x99, 0x99]);
|
||||
|
||||
let mut response = main_hid.process_hid_packet(&mut env, &ping_packet, &mut ctap_state);
|
||||
assert_eq!(response.next(), Some(ping_packet));
|
||||
assert_eq!(response.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_hid_packet_empty() {
|
||||
let mut env = TestEnv::default();
|
||||
let mut ctap_state = CtapState::<TestEnv>::new(&mut env);
|
||||
let (mut main_hid, cid) = new_initialized();
|
||||
|
||||
let mut cancel_packet = [0x00; 64];
|
||||
cancel_packet[..4].copy_from_slice(&cid);
|
||||
cancel_packet[4..7].copy_from_slice(&[0x91, 0x00, 0x00]);
|
||||
|
||||
let mut response = main_hid.process_hid_packet(&mut env, &cancel_packet, &mut ctap_state);
|
||||
assert_eq!(response.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wink() {
|
||||
let mut env = TestEnv::default();
|
||||
let mut ctap_state = CtapState::<TestEnv>::new(&mut env);
|
||||
let (mut main_hid, cid) = new_initialized();
|
||||
assert!(!main_hid.should_wink(&mut env));
|
||||
|
||||
let mut wink_packet = [0x00; 64];
|
||||
wink_packet[..4].copy_from_slice(&cid);
|
||||
wink_packet[4..7].copy_from_slice(&[0x88, 0x00, 0x00]);
|
||||
|
||||
let mut response = main_hid.process_hid_packet(&mut env, &wink_packet, &mut ctap_state);
|
||||
assert_eq!(response.next(), Some(wink_packet));
|
||||
assert_eq!(response.next(), None);
|
||||
assert!(main_hid.should_wink(&mut env));
|
||||
env.clock().advance(WINK_TIMEOUT_DURATION_MS);
|
||||
assert!(!main_hid.should_wink(&mut env));
|
||||
}
|
||||
}
|
||||
3747
libraries/opensk/src/ctap/mod.rs
Normal file
3747
libraries/opensk/src/ctap/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
408
libraries/opensk/src/ctap/pin_protocol.rs
Normal file
408
libraries/opensk/src/ctap/pin_protocol.rs
Normal file
@@ -0,0 +1,408 @@
|
||||
// Copyright 2021 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::ctap::client_pin::PIN_TOKEN_LENGTH;
|
||||
use crate::ctap::crypto_wrapper::{aes256_cbc_decrypt, aes256_cbc_encrypt};
|
||||
use crate::ctap::data_formats::{CoseKey, PinUvAuthProtocol};
|
||||
use crate::ctap::status_code::Ctap2StatusCode;
|
||||
use alloc::boxed::Box;
|
||||
use alloc::vec::Vec;
|
||||
use core::convert::TryInto;
|
||||
use crypto::hkdf::hkdf_empty_salt_256;
|
||||
#[cfg(test)]
|
||||
use crypto::hmac::hmac_256;
|
||||
use crypto::hmac::{verify_hmac_256, verify_hmac_256_first_128bits};
|
||||
use crypto::sha256::Sha256;
|
||||
use crypto::Hash256;
|
||||
use rng256::Rng256;
|
||||
|
||||
/// Implements common functions between existing PIN protocols for handshakes.
|
||||
pub struct PinProtocol {
|
||||
key_agreement_key: crypto::ecdh::SecKey,
|
||||
pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH],
|
||||
}
|
||||
|
||||
impl PinProtocol {
|
||||
/// This process is run by the authenticator at power-on.
|
||||
///
|
||||
/// This function implements "initialize" from the specification.
|
||||
pub fn new(rng: &mut impl Rng256) -> PinProtocol {
|
||||
let key_agreement_key = crypto::ecdh::SecKey::gensk(rng);
|
||||
let pin_uv_auth_token = rng.gen_uniform_u8x32();
|
||||
PinProtocol {
|
||||
key_agreement_key,
|
||||
pin_uv_auth_token,
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a fresh public key.
|
||||
pub fn regenerate(&mut self, rng: &mut impl Rng256) {
|
||||
self.key_agreement_key = crypto::ecdh::SecKey::gensk(rng);
|
||||
}
|
||||
|
||||
/// Generates a fresh pinUvAuthToken.
|
||||
pub fn reset_pin_uv_auth_token(&mut self, rng: &mut impl Rng256) {
|
||||
self.pin_uv_auth_token = rng.gen_uniform_u8x32();
|
||||
}
|
||||
|
||||
/// Returns the authenticator’s public key as a CoseKey structure.
|
||||
pub fn get_public_key(&self) -> CoseKey {
|
||||
CoseKey::from(self.key_agreement_key.genpk())
|
||||
}
|
||||
|
||||
/// Processes the peer's encapsulated CoseKey and returns the shared secret.
|
||||
pub fn decapsulate(
|
||||
&self,
|
||||
peer_cose_key: CoseKey,
|
||||
pin_uv_auth_protocol: PinUvAuthProtocol,
|
||||
) -> Result<Box<dyn SharedSecret>, Ctap2StatusCode> {
|
||||
let pk: crypto::ecdh::PubKey = CoseKey::try_into(peer_cose_key)?;
|
||||
let handshake = self.key_agreement_key.exchange_x(&pk);
|
||||
match pin_uv_auth_protocol {
|
||||
PinUvAuthProtocol::V1 => Ok(Box::new(SharedSecretV1::new(handshake))),
|
||||
PinUvAuthProtocol::V2 => Ok(Box::new(SharedSecretV2::new(handshake))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Getter for pinUvAuthToken.
|
||||
pub fn get_pin_uv_auth_token(&self) -> &[u8; PIN_TOKEN_LENGTH] {
|
||||
&self.pin_uv_auth_token
|
||||
}
|
||||
|
||||
/// This is used for debugging to inject key material.
|
||||
#[cfg(test)]
|
||||
pub fn new_test(
|
||||
key_agreement_key: crypto::ecdh::SecKey,
|
||||
pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH],
|
||||
) -> PinProtocol {
|
||||
PinProtocol {
|
||||
key_agreement_key,
|
||||
pin_uv_auth_token,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Authenticates the pinUvAuthToken for the given PIN protocol.
|
||||
#[cfg(test)]
|
||||
pub fn authenticate_pin_uv_auth_token(
|
||||
token: &[u8; PIN_TOKEN_LENGTH],
|
||||
message: &[u8],
|
||||
pin_uv_auth_protocol: PinUvAuthProtocol,
|
||||
) -> Vec<u8> {
|
||||
match pin_uv_auth_protocol {
|
||||
PinUvAuthProtocol::V1 => hmac_256::<Sha256>(token, message)[..16].to_vec(),
|
||||
PinUvAuthProtocol::V2 => hmac_256::<Sha256>(token, message).to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifies the pinUvAuthToken for the given PIN protocol.
|
||||
pub fn verify_pin_uv_auth_token(
|
||||
token: &[u8; PIN_TOKEN_LENGTH],
|
||||
message: &[u8],
|
||||
signature: &[u8],
|
||||
pin_uv_auth_protocol: PinUvAuthProtocol,
|
||||
) -> Result<(), Ctap2StatusCode> {
|
||||
match pin_uv_auth_protocol {
|
||||
PinUvAuthProtocol::V1 => verify_v1(token, message, signature),
|
||||
PinUvAuthProtocol::V2 => verify_v2(token, message, signature),
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SharedSecret {
|
||||
/// Returns the encrypted plaintext.
|
||||
fn encrypt(&self, rng: &mut dyn Rng256, plaintext: &[u8]) -> Result<Vec<u8>, Ctap2StatusCode>;
|
||||
|
||||
/// Returns the decrypted ciphertext.
|
||||
fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, Ctap2StatusCode>;
|
||||
|
||||
/// Verifies that the signature is a valid MAC for the given message.
|
||||
fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode>;
|
||||
|
||||
/// Creates a signature that matches verify.
|
||||
#[cfg(test)]
|
||||
fn authenticate(&self, message: &[u8]) -> Vec<u8>;
|
||||
}
|
||||
|
||||
fn verify_v1(key: &[u8; 32], message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode> {
|
||||
if signature.len() != 16 {
|
||||
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
|
||||
}
|
||||
if verify_hmac_256_first_128bits::<Sha256>(key, message, array_ref![signature, 0, 16]) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_v2(key: &[u8; 32], message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode> {
|
||||
if signature.len() != 32 {
|
||||
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
|
||||
}
|
||||
if verify_hmac_256::<Sha256>(key, message, array_ref![signature, 0, 32]) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SharedSecretV1 {
|
||||
common_secret: [u8; 32],
|
||||
aes_enc_key: crypto::aes256::EncryptionKey,
|
||||
}
|
||||
|
||||
impl SharedSecretV1 {
|
||||
/// Creates a new shared secret from the handshake result.
|
||||
fn new(handshake: [u8; 32]) -> SharedSecretV1 {
|
||||
let common_secret = Sha256::hash(&handshake);
|
||||
let aes_enc_key = crypto::aes256::EncryptionKey::new(&common_secret);
|
||||
SharedSecretV1 {
|
||||
common_secret,
|
||||
aes_enc_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SharedSecret for SharedSecretV1 {
|
||||
fn encrypt(&self, rng: &mut dyn Rng256, plaintext: &[u8]) -> Result<Vec<u8>, Ctap2StatusCode> {
|
||||
aes256_cbc_encrypt(rng, &self.aes_enc_key, plaintext, false)
|
||||
}
|
||||
|
||||
fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, Ctap2StatusCode> {
|
||||
aes256_cbc_decrypt(&self.aes_enc_key, ciphertext, false)
|
||||
}
|
||||
|
||||
fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode> {
|
||||
verify_v1(&self.common_secret, message, signature)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn authenticate(&self, message: &[u8]) -> Vec<u8> {
|
||||
hmac_256::<Sha256>(&self.common_secret, message)[..16].to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SharedSecretV2 {
|
||||
aes_enc_key: crypto::aes256::EncryptionKey,
|
||||
hmac_key: [u8; 32],
|
||||
}
|
||||
|
||||
impl SharedSecretV2 {
|
||||
/// Creates a new shared secret from the handshake result.
|
||||
fn new(handshake: [u8; 32]) -> SharedSecretV2 {
|
||||
let aes_key = hkdf_empty_salt_256::<Sha256>(&handshake, b"CTAP2 AES key");
|
||||
SharedSecretV2 {
|
||||
aes_enc_key: crypto::aes256::EncryptionKey::new(&aes_key),
|
||||
hmac_key: hkdf_empty_salt_256::<Sha256>(&handshake, b"CTAP2 HMAC key"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SharedSecret for SharedSecretV2 {
|
||||
fn encrypt(&self, rng: &mut dyn Rng256, plaintext: &[u8]) -> Result<Vec<u8>, Ctap2StatusCode> {
|
||||
aes256_cbc_encrypt(rng, &self.aes_enc_key, plaintext, true)
|
||||
}
|
||||
|
||||
fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, Ctap2StatusCode> {
|
||||
aes256_cbc_decrypt(&self.aes_enc_key, ciphertext, true)
|
||||
}
|
||||
|
||||
fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode> {
|
||||
verify_v2(&self.hmac_key, message, signature)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn authenticate(&self, message: &[u8]) -> Vec<u8> {
|
||||
hmac_256::<Sha256>(&self.hmac_key, message).to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::env::test::TestEnv;
|
||||
|
||||
#[test]
|
||||
fn test_pin_protocol_public_key() {
|
||||
let mut env = TestEnv::default();
|
||||
let mut pin_protocol = PinProtocol::new(env.rng());
|
||||
let public_key = pin_protocol.get_public_key();
|
||||
pin_protocol.regenerate(env.rng());
|
||||
let new_public_key = pin_protocol.get_public_key();
|
||||
assert_ne!(public_key, new_public_key);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pin_protocol_pin_uv_auth_token() {
|
||||
let mut env = TestEnv::default();
|
||||
let mut pin_protocol = PinProtocol::new(env.rng());
|
||||
let token = *pin_protocol.get_pin_uv_auth_token();
|
||||
pin_protocol.reset_pin_uv_auth_token(env.rng());
|
||||
let new_token = pin_protocol.get_pin_uv_auth_token();
|
||||
assert_ne!(&token, new_token);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shared_secret_v1_encrypt_decrypt() {
|
||||
let mut env = TestEnv::default();
|
||||
let shared_secret = SharedSecretV1::new([0x55; 32]);
|
||||
let plaintext = vec![0xAA; 64];
|
||||
let ciphertext = shared_secret.encrypt(env.rng(), &plaintext).unwrap();
|
||||
assert_eq!(shared_secret.decrypt(&ciphertext), Ok(plaintext));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shared_secret_v1_authenticate_verify() {
|
||||
let shared_secret = SharedSecretV1::new([0x55; 32]);
|
||||
let message = [0xAA; 32];
|
||||
let signature = shared_secret.authenticate(&message);
|
||||
assert_eq!(shared_secret.verify(&message, &signature), Ok(()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shared_secret_v1_verify() {
|
||||
let shared_secret = SharedSecretV1::new([0x55; 32]);
|
||||
let message = [0xAA];
|
||||
let signature = [
|
||||
0x8B, 0x60, 0x15, 0x7D, 0xF3, 0x44, 0x82, 0x2E, 0x54, 0x34, 0x7A, 0x01, 0xFB, 0x02,
|
||||
0x48, 0xA6,
|
||||
];
|
||||
assert_eq!(shared_secret.verify(&message, &signature), Ok(()));
|
||||
assert_eq!(
|
||||
shared_secret.verify(&[0xBB], &signature),
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
|
||||
);
|
||||
assert_eq!(
|
||||
shared_secret.verify(&message, &[0x12; 16]),
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shared_secret_v2_encrypt_decrypt() {
|
||||
let mut env = TestEnv::default();
|
||||
let shared_secret = SharedSecretV2::new([0x55; 32]);
|
||||
let plaintext = vec![0xAA; 64];
|
||||
let ciphertext = shared_secret.encrypt(env.rng(), &plaintext).unwrap();
|
||||
assert_eq!(shared_secret.decrypt(&ciphertext), Ok(plaintext));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shared_secret_v2_authenticate_verify() {
|
||||
let shared_secret = SharedSecretV2::new([0x55; 32]);
|
||||
let message = [0xAA; 32];
|
||||
let signature = shared_secret.authenticate(&message);
|
||||
assert_eq!(shared_secret.verify(&message, &signature), Ok(()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shared_secret_v2_verify() {
|
||||
let shared_secret = SharedSecretV2::new([0x55; 32]);
|
||||
let message = [0xAA];
|
||||
let signature = [
|
||||
0xC0, 0x3F, 0x2A, 0x22, 0x5C, 0xC3, 0x4E, 0x05, 0xC1, 0x0E, 0x72, 0x9C, 0x8D, 0xD5,
|
||||
0x7D, 0xE5, 0x98, 0x9C, 0x68, 0x15, 0xEC, 0xE2, 0x3A, 0x95, 0xD5, 0x90, 0xE1, 0xE9,
|
||||
0x3F, 0xF0, 0x1A, 0xAF,
|
||||
];
|
||||
assert_eq!(shared_secret.verify(&message, &signature), Ok(()));
|
||||
assert_eq!(
|
||||
shared_secret.verify(&[0xBB], &signature),
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
|
||||
);
|
||||
assert_eq!(
|
||||
shared_secret.verify(&message, &[0x12; 32]),
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decapsulate_symmetric() {
|
||||
let mut env = TestEnv::default();
|
||||
let pin_protocol1 = PinProtocol::new(env.rng());
|
||||
let pin_protocol2 = PinProtocol::new(env.rng());
|
||||
for &protocol in &[PinUvAuthProtocol::V1, PinUvAuthProtocol::V2] {
|
||||
let shared_secret1 = pin_protocol1
|
||||
.decapsulate(pin_protocol2.get_public_key(), protocol)
|
||||
.unwrap();
|
||||
let shared_secret2 = pin_protocol2
|
||||
.decapsulate(pin_protocol1.get_public_key(), protocol)
|
||||
.unwrap();
|
||||
let plaintext = vec![0xAA; 64];
|
||||
let ciphertext = shared_secret1.encrypt(env.rng(), &plaintext).unwrap();
|
||||
assert_eq!(plaintext, shared_secret2.decrypt(&ciphertext).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_pin_uv_auth_token_v1() {
|
||||
let token = [0x91; PIN_TOKEN_LENGTH];
|
||||
let message = [0xAA];
|
||||
let signature = [
|
||||
0x9C, 0x1C, 0xFE, 0x9D, 0xD7, 0x64, 0x6A, 0x06, 0xB9, 0xA8, 0x0F, 0x96, 0xAD, 0x50,
|
||||
0x49, 0x68,
|
||||
];
|
||||
assert_eq!(
|
||||
verify_pin_uv_auth_token(&token, &message, &signature, PinUvAuthProtocol::V1),
|
||||
Ok(())
|
||||
);
|
||||
assert_eq!(
|
||||
verify_pin_uv_auth_token(
|
||||
&[0x12; PIN_TOKEN_LENGTH],
|
||||
&message,
|
||||
&signature,
|
||||
PinUvAuthProtocol::V1
|
||||
),
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
|
||||
);
|
||||
assert_eq!(
|
||||
verify_pin_uv_auth_token(&token, &[0xBB], &signature, PinUvAuthProtocol::V1),
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
|
||||
);
|
||||
assert_eq!(
|
||||
verify_pin_uv_auth_token(&token, &message, &[0x12; 16], PinUvAuthProtocol::V1),
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_pin_uv_auth_token_v2() {
|
||||
let token = [0x91; PIN_TOKEN_LENGTH];
|
||||
let message = [0xAA];
|
||||
let signature = [
|
||||
0x9C, 0x1C, 0xFE, 0x9D, 0xD7, 0x64, 0x6A, 0x06, 0xB9, 0xA8, 0x0F, 0x96, 0xAD, 0x50,
|
||||
0x49, 0x68, 0x94, 0x90, 0x20, 0x53, 0x0F, 0xA3, 0xD2, 0x7A, 0x9F, 0xFD, 0xFA, 0x62,
|
||||
0x36, 0x93, 0xF7, 0x84,
|
||||
];
|
||||
assert_eq!(
|
||||
verify_pin_uv_auth_token(&token, &message, &signature, PinUvAuthProtocol::V2),
|
||||
Ok(())
|
||||
);
|
||||
assert_eq!(
|
||||
verify_pin_uv_auth_token(
|
||||
&[0x12; PIN_TOKEN_LENGTH],
|
||||
&message,
|
||||
&signature,
|
||||
PinUvAuthProtocol::V2
|
||||
),
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
|
||||
);
|
||||
assert_eq!(
|
||||
verify_pin_uv_auth_token(&token, &[0xBB], &signature, PinUvAuthProtocol::V2),
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
|
||||
);
|
||||
assert_eq!(
|
||||
verify_pin_uv_auth_token(&token, &message, &[0x12; 32], PinUvAuthProtocol::V2),
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
|
||||
);
|
||||
}
|
||||
}
|
||||
688
libraries/opensk/src/ctap/response.rs
Normal file
688
libraries/opensk/src/ctap/response.rs
Normal file
@@ -0,0 +1,688 @@
|
||||
// Copyright 2019-2021 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::data_formats::{
|
||||
AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, PackedAttestationStatement,
|
||||
PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialRpEntity,
|
||||
PublicKeyCredentialUserEntity,
|
||||
};
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use sk_cbor as cbor;
|
||||
use sk_cbor::{
|
||||
cbor_array_vec, cbor_bool, cbor_int, cbor_map_collection, cbor_map_options, cbor_text,
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub enum ResponseData {
|
||||
AuthenticatorMakeCredential(AuthenticatorMakeCredentialResponse),
|
||||
AuthenticatorGetAssertion(AuthenticatorGetAssertionResponse),
|
||||
AuthenticatorGetNextAssertion(AuthenticatorGetAssertionResponse),
|
||||
AuthenticatorGetInfo(AuthenticatorGetInfoResponse),
|
||||
AuthenticatorClientPin(Option<AuthenticatorClientPinResponse>),
|
||||
AuthenticatorReset,
|
||||
AuthenticatorCredentialManagement(Option<AuthenticatorCredentialManagementResponse>),
|
||||
AuthenticatorSelection,
|
||||
AuthenticatorLargeBlobs(Option<AuthenticatorLargeBlobsResponse>),
|
||||
AuthenticatorConfig,
|
||||
AuthenticatorVendorConfigure(AuthenticatorVendorConfigureResponse),
|
||||
AuthenticatorVendorUpgrade,
|
||||
AuthenticatorVendorUpgradeInfo(AuthenticatorVendorUpgradeInfoResponse),
|
||||
}
|
||||
|
||||
impl From<ResponseData> for Option<cbor::Value> {
|
||||
fn from(response: ResponseData) -> Self {
|
||||
match response {
|
||||
ResponseData::AuthenticatorMakeCredential(data) => Some(data.into()),
|
||||
ResponseData::AuthenticatorGetAssertion(data) => Some(data.into()),
|
||||
ResponseData::AuthenticatorGetNextAssertion(data) => Some(data.into()),
|
||||
ResponseData::AuthenticatorGetInfo(data) => Some(data.into()),
|
||||
ResponseData::AuthenticatorClientPin(data) => data.map(|d| d.into()),
|
||||
ResponseData::AuthenticatorReset => None,
|
||||
ResponseData::AuthenticatorCredentialManagement(data) => data.map(|d| d.into()),
|
||||
ResponseData::AuthenticatorSelection => None,
|
||||
ResponseData::AuthenticatorLargeBlobs(data) => data.map(|d| d.into()),
|
||||
ResponseData::AuthenticatorConfig => None,
|
||||
ResponseData::AuthenticatorVendorConfigure(data) => Some(data.into()),
|
||||
ResponseData::AuthenticatorVendorUpgrade => None,
|
||||
ResponseData::AuthenticatorVendorUpgradeInfo(data) => Some(data.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct AuthenticatorMakeCredentialResponse {
|
||||
pub fmt: String,
|
||||
pub auth_data: Vec<u8>,
|
||||
pub att_stmt: PackedAttestationStatement,
|
||||
pub ep_att: Option<bool>,
|
||||
pub large_blob_key: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl From<AuthenticatorMakeCredentialResponse> for cbor::Value {
|
||||
fn from(make_credential_response: AuthenticatorMakeCredentialResponse) -> Self {
|
||||
let AuthenticatorMakeCredentialResponse {
|
||||
fmt,
|
||||
auth_data,
|
||||
att_stmt,
|
||||
ep_att,
|
||||
large_blob_key,
|
||||
} = make_credential_response;
|
||||
|
||||
cbor_map_options! {
|
||||
0x01 => fmt,
|
||||
0x02 => auth_data,
|
||||
0x03 => att_stmt,
|
||||
0x04 => ep_att,
|
||||
0x05 => large_blob_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct AuthenticatorGetAssertionResponse {
|
||||
pub credential: Option<PublicKeyCredentialDescriptor>,
|
||||
pub auth_data: Vec<u8>,
|
||||
pub signature: Vec<u8>,
|
||||
pub user: Option<PublicKeyCredentialUserEntity>,
|
||||
pub number_of_credentials: Option<u64>,
|
||||
// 0x06: userSelected missing as we don't support displays.
|
||||
pub large_blob_key: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl From<AuthenticatorGetAssertionResponse> for cbor::Value {
|
||||
fn from(get_assertion_response: AuthenticatorGetAssertionResponse) -> Self {
|
||||
let AuthenticatorGetAssertionResponse {
|
||||
credential,
|
||||
auth_data,
|
||||
signature,
|
||||
user,
|
||||
number_of_credentials,
|
||||
large_blob_key,
|
||||
} = get_assertion_response;
|
||||
|
||||
cbor_map_options! {
|
||||
0x01 => credential,
|
||||
0x02 => auth_data,
|
||||
0x03 => signature,
|
||||
0x04 => user,
|
||||
0x05 => number_of_credentials,
|
||||
0x07 => large_blob_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct AuthenticatorGetInfoResponse {
|
||||
pub versions: Vec<String>,
|
||||
pub extensions: Option<Vec<String>>,
|
||||
pub aaguid: [u8; 16],
|
||||
pub options: Option<Vec<(String, bool)>>,
|
||||
pub max_msg_size: Option<u64>,
|
||||
pub pin_protocols: Option<Vec<u64>>,
|
||||
pub max_credential_count_in_list: Option<u64>,
|
||||
pub max_credential_id_length: Option<u64>,
|
||||
pub transports: Option<Vec<AuthenticatorTransport>>,
|
||||
pub algorithms: Option<Vec<PublicKeyCredentialParameter>>,
|
||||
pub max_serialized_large_blob_array: Option<u64>,
|
||||
pub force_pin_change: Option<bool>,
|
||||
pub min_pin_length: u8,
|
||||
pub firmware_version: Option<u64>,
|
||||
pub max_cred_blob_length: Option<u64>,
|
||||
pub max_rp_ids_for_set_min_pin_length: Option<u64>,
|
||||
// Missing response fields as they are only relevant for internal UV:
|
||||
// - 0x11: preferredPlatformUvAttempts
|
||||
// - 0x12: uvModality
|
||||
// Add them when your hardware supports any kind of user verification within
|
||||
// the boundary of the device, e.g. fingerprint or built-in keyboard.
|
||||
pub certifications: Option<Vec<(String, i64)>>,
|
||||
pub remaining_discoverable_credentials: Option<u64>,
|
||||
// - 0x15: vendorPrototypeConfigCommands missing as we don't support it.
|
||||
}
|
||||
|
||||
impl From<AuthenticatorGetInfoResponse> for cbor::Value {
|
||||
fn from(get_info_response: AuthenticatorGetInfoResponse) -> Self {
|
||||
let AuthenticatorGetInfoResponse {
|
||||
versions,
|
||||
extensions,
|
||||
aaguid,
|
||||
options,
|
||||
max_msg_size,
|
||||
pin_protocols,
|
||||
max_credential_count_in_list,
|
||||
max_credential_id_length,
|
||||
transports,
|
||||
algorithms,
|
||||
max_serialized_large_blob_array,
|
||||
force_pin_change,
|
||||
min_pin_length,
|
||||
firmware_version,
|
||||
max_cred_blob_length,
|
||||
max_rp_ids_for_set_min_pin_length,
|
||||
certifications,
|
||||
remaining_discoverable_credentials,
|
||||
} = get_info_response;
|
||||
|
||||
let options_cbor: Option<cbor::Value> = options.map(|options| {
|
||||
let options_map: Vec<(_, _)> = options
|
||||
.into_iter()
|
||||
.map(|(key, value)| (cbor_text!(key), cbor_bool!(value)))
|
||||
.collect();
|
||||
cbor_map_collection!(options_map)
|
||||
});
|
||||
|
||||
let certifications_cbor: Option<cbor::Value> = certifications.map(|certifications| {
|
||||
let certifications_map: Vec<(_, _)> = certifications
|
||||
.into_iter()
|
||||
.map(|(key, value)| (cbor_text!(key), cbor_int!(value)))
|
||||
.collect();
|
||||
cbor_map_collection!(certifications_map)
|
||||
});
|
||||
|
||||
cbor_map_options! {
|
||||
0x01 => cbor_array_vec!(versions),
|
||||
0x02 => extensions.map(|vec| cbor_array_vec!(vec)),
|
||||
0x03 => &aaguid,
|
||||
0x04 => options_cbor,
|
||||
0x05 => max_msg_size,
|
||||
0x06 => pin_protocols.map(|vec| cbor_array_vec!(vec)),
|
||||
0x07 => max_credential_count_in_list,
|
||||
0x08 => max_credential_id_length,
|
||||
0x09 => transports.map(|vec| cbor_array_vec!(vec)),
|
||||
0x0A => algorithms.map(|vec| cbor_array_vec!(vec)),
|
||||
0x0B => max_serialized_large_blob_array,
|
||||
0x0C => force_pin_change,
|
||||
0x0D => min_pin_length as u64,
|
||||
0x0E => firmware_version,
|
||||
0x0F => max_cred_blob_length,
|
||||
0x10 => max_rp_ids_for_set_min_pin_length,
|
||||
0x13 => certifications_cbor,
|
||||
0x14 => remaining_discoverable_credentials,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct AuthenticatorClientPinResponse {
|
||||
pub key_agreement: Option<CoseKey>,
|
||||
pub pin_uv_auth_token: Option<Vec<u8>>,
|
||||
pub retries: Option<u64>,
|
||||
pub power_cycle_state: Option<bool>,
|
||||
// - 0x05: uvRetries missing as we don't support internal UV.
|
||||
}
|
||||
|
||||
impl From<AuthenticatorClientPinResponse> for cbor::Value {
|
||||
fn from(client_pin_response: AuthenticatorClientPinResponse) -> Self {
|
||||
let AuthenticatorClientPinResponse {
|
||||
key_agreement,
|
||||
pin_uv_auth_token,
|
||||
retries,
|
||||
power_cycle_state,
|
||||
} = client_pin_response;
|
||||
|
||||
cbor_map_options! {
|
||||
0x01 => key_agreement.map(cbor::Value::from),
|
||||
0x02 => pin_uv_auth_token,
|
||||
0x03 => retries,
|
||||
0x04 => power_cycle_state,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct AuthenticatorLargeBlobsResponse {
|
||||
pub config: Vec<u8>,
|
||||
}
|
||||
|
||||
impl From<AuthenticatorLargeBlobsResponse> for cbor::Value {
|
||||
fn from(platform_large_blobs_response: AuthenticatorLargeBlobsResponse) -> Self {
|
||||
let AuthenticatorLargeBlobsResponse { config } = platform_large_blobs_response;
|
||||
|
||||
cbor_map_options! {
|
||||
0x01 => config,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub struct AuthenticatorCredentialManagementResponse {
|
||||
pub existing_resident_credentials_count: Option<u64>,
|
||||
pub max_possible_remaining_resident_credentials_count: Option<u64>,
|
||||
pub rp: Option<PublicKeyCredentialRpEntity>,
|
||||
pub rp_id_hash: Option<Vec<u8>>,
|
||||
pub total_rps: Option<u64>,
|
||||
pub user: Option<PublicKeyCredentialUserEntity>,
|
||||
pub credential_id: Option<PublicKeyCredentialDescriptor>,
|
||||
pub public_key: Option<CoseKey>,
|
||||
pub total_credentials: Option<u64>,
|
||||
pub cred_protect: Option<CredentialProtectionPolicy>,
|
||||
pub large_blob_key: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl From<AuthenticatorCredentialManagementResponse> for cbor::Value {
|
||||
fn from(cred_management_response: AuthenticatorCredentialManagementResponse) -> Self {
|
||||
let AuthenticatorCredentialManagementResponse {
|
||||
existing_resident_credentials_count,
|
||||
max_possible_remaining_resident_credentials_count,
|
||||
rp,
|
||||
rp_id_hash,
|
||||
total_rps,
|
||||
user,
|
||||
credential_id,
|
||||
public_key,
|
||||
total_credentials,
|
||||
cred_protect,
|
||||
large_blob_key,
|
||||
} = cred_management_response;
|
||||
|
||||
cbor_map_options! {
|
||||
0x01 => existing_resident_credentials_count,
|
||||
0x02 => max_possible_remaining_resident_credentials_count,
|
||||
0x03 => rp,
|
||||
0x04 => rp_id_hash,
|
||||
0x05 => total_rps,
|
||||
0x06 => user,
|
||||
0x07 => credential_id,
|
||||
0x08 => public_key.map(cbor::Value::from),
|
||||
0x09 => total_credentials,
|
||||
0x0A => cred_protect,
|
||||
0x0B => large_blob_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct AuthenticatorVendorConfigureResponse {
|
||||
pub cert_programmed: bool,
|
||||
pub pkey_programmed: bool,
|
||||
}
|
||||
|
||||
impl From<AuthenticatorVendorConfigureResponse> for cbor::Value {
|
||||
fn from(vendor_response: AuthenticatorVendorConfigureResponse) -> Self {
|
||||
let AuthenticatorVendorConfigureResponse {
|
||||
cert_programmed,
|
||||
pkey_programmed,
|
||||
} = vendor_response;
|
||||
|
||||
cbor_map_options! {
|
||||
0x01 => cert_programmed,
|
||||
0x02 => pkey_programmed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct AuthenticatorVendorUpgradeInfoResponse {
|
||||
pub info: u32,
|
||||
}
|
||||
|
||||
impl From<AuthenticatorVendorUpgradeInfoResponse> for cbor::Value {
|
||||
fn from(vendor_upgrade_info_response: AuthenticatorVendorUpgradeInfoResponse) -> Self {
|
||||
let AuthenticatorVendorUpgradeInfoResponse { info } = vendor_upgrade_info_response;
|
||||
|
||||
cbor_map_options! {
|
||||
0x01 => info as u64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::super::data_formats::{PackedAttestationStatement, PublicKeyCredentialType};
|
||||
use super::super::ES256_CRED_PARAM;
|
||||
use super::*;
|
||||
use crate::env::test::TestEnv;
|
||||
use cbor::{cbor_array, cbor_bytes, cbor_map};
|
||||
|
||||
#[test]
|
||||
fn test_make_credential_into_cbor() {
|
||||
let certificate = cbor_bytes![vec![0x5C, 0x5C, 0x5C, 0x5C]];
|
||||
let att_stmt = PackedAttestationStatement {
|
||||
alg: 1,
|
||||
sig: vec![0x55, 0x55, 0x55, 0x55],
|
||||
x5c: Some(vec![vec![0x5C, 0x5C, 0x5C, 0x5C]]),
|
||||
ecdaa_key_id: Some(vec![0xEC, 0xDA, 0x1D]),
|
||||
};
|
||||
let cbor_packed_attestation_statement = cbor_map! {
|
||||
"alg" => 1,
|
||||
"sig" => vec![0x55, 0x55, 0x55, 0x55],
|
||||
"x5c" => cbor_array![certificate],
|
||||
"ecdaaKeyId" => vec![0xEC, 0xDA, 0x1D],
|
||||
};
|
||||
|
||||
let make_credential_response = AuthenticatorMakeCredentialResponse {
|
||||
fmt: "packed".to_string(),
|
||||
auth_data: vec![0xAD],
|
||||
att_stmt,
|
||||
ep_att: Some(true),
|
||||
large_blob_key: Some(vec![0x1B]),
|
||||
};
|
||||
let response_cbor: Option<cbor::Value> =
|
||||
ResponseData::AuthenticatorMakeCredential(make_credential_response).into();
|
||||
let expected_cbor = cbor_map_options! {
|
||||
0x01 => "packed",
|
||||
0x02 => vec![0xAD],
|
||||
0x03 => cbor_packed_attestation_statement,
|
||||
0x04 => true,
|
||||
0x05 => vec![0x1B],
|
||||
};
|
||||
assert_eq!(response_cbor, Some(expected_cbor));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_assertion_into_cbor() {
|
||||
let pub_key_cred_descriptor = PublicKeyCredentialDescriptor {
|
||||
key_type: PublicKeyCredentialType::PublicKey,
|
||||
key_id: vec![0x2D, 0x2D, 0x2D, 0x2D],
|
||||
transports: Some(vec![AuthenticatorTransport::Usb]),
|
||||
};
|
||||
let user = PublicKeyCredentialUserEntity {
|
||||
user_id: vec![0x1D, 0x1D, 0x1D, 0x1D],
|
||||
user_name: Some("foo".to_string()),
|
||||
user_display_name: Some("bar".to_string()),
|
||||
user_icon: Some("example.com/foo/icon.png".to_string()),
|
||||
};
|
||||
let get_assertion_response = AuthenticatorGetAssertionResponse {
|
||||
credential: Some(pub_key_cred_descriptor),
|
||||
auth_data: vec![0xAD],
|
||||
signature: vec![0x51],
|
||||
user: Some(user),
|
||||
number_of_credentials: Some(2),
|
||||
large_blob_key: Some(vec![0x1B]),
|
||||
};
|
||||
let response_cbor: Option<cbor::Value> =
|
||||
ResponseData::AuthenticatorGetAssertion(get_assertion_response).into();
|
||||
let expected_cbor = cbor_map_options! {
|
||||
0x01 => cbor_map! {
|
||||
"id" => vec![0x2D, 0x2D, 0x2D, 0x2D],
|
||||
"type" => "public-key",
|
||||
"transports" => cbor_array!["usb"],
|
||||
},
|
||||
0x02 => vec![0xAD],
|
||||
0x03 => vec![0x51],
|
||||
0x04 => cbor_map! {
|
||||
"id" => vec![0x1D, 0x1D, 0x1D, 0x1D],
|
||||
"icon" => "example.com/foo/icon.png".to_string(),
|
||||
"name" => "foo".to_string(),
|
||||
"displayName" => "bar".to_string(),
|
||||
},
|
||||
0x05 => 2,
|
||||
0x07 => vec![0x1B],
|
||||
};
|
||||
assert_eq!(response_cbor, Some(expected_cbor));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_info_into_cbor() {
|
||||
let versions = vec!["FIDO_2_0".to_string()];
|
||||
let get_info_response = AuthenticatorGetInfoResponse {
|
||||
versions: versions.clone(),
|
||||
extensions: None,
|
||||
aaguid: [0x00; 16],
|
||||
options: None,
|
||||
max_msg_size: None,
|
||||
pin_protocols: None,
|
||||
max_credential_count_in_list: None,
|
||||
max_credential_id_length: None,
|
||||
transports: None,
|
||||
algorithms: None,
|
||||
max_serialized_large_blob_array: None,
|
||||
force_pin_change: None,
|
||||
min_pin_length: 4,
|
||||
firmware_version: None,
|
||||
max_cred_blob_length: None,
|
||||
max_rp_ids_for_set_min_pin_length: None,
|
||||
certifications: None,
|
||||
remaining_discoverable_credentials: None,
|
||||
};
|
||||
let response_cbor: Option<cbor::Value> =
|
||||
ResponseData::AuthenticatorGetInfo(get_info_response).into();
|
||||
let expected_cbor = cbor_map_options! {
|
||||
0x01 => cbor_array_vec![versions],
|
||||
0x03 => vec![0x00; 16],
|
||||
0x0D => 4,
|
||||
};
|
||||
assert_eq!(response_cbor, Some(expected_cbor));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_info_optionals_into_cbor() {
|
||||
let get_info_response = AuthenticatorGetInfoResponse {
|
||||
versions: vec!["FIDO_2_0".to_string()],
|
||||
extensions: Some(vec!["extension".to_string()]),
|
||||
aaguid: [0x00; 16],
|
||||
options: Some(vec![(String::from("rk"), true)]),
|
||||
max_msg_size: Some(1024),
|
||||
pin_protocols: Some(vec![1]),
|
||||
max_credential_count_in_list: Some(20),
|
||||
max_credential_id_length: Some(256),
|
||||
transports: Some(vec![AuthenticatorTransport::Usb]),
|
||||
algorithms: Some(vec![ES256_CRED_PARAM]),
|
||||
max_serialized_large_blob_array: Some(1024),
|
||||
force_pin_change: Some(false),
|
||||
min_pin_length: 4,
|
||||
firmware_version: Some(0),
|
||||
max_cred_blob_length: Some(1024),
|
||||
max_rp_ids_for_set_min_pin_length: Some(8),
|
||||
certifications: Some(vec![(String::from("example-cert"), 1)]),
|
||||
remaining_discoverable_credentials: Some(150),
|
||||
};
|
||||
let response_cbor: Option<cbor::Value> =
|
||||
ResponseData::AuthenticatorGetInfo(get_info_response).into();
|
||||
let expected_cbor = cbor_map_options! {
|
||||
0x01 => cbor_array!["FIDO_2_0"],
|
||||
0x02 => cbor_array!["extension"],
|
||||
0x03 => vec![0x00; 16],
|
||||
0x04 => cbor_map! {"rk" => true},
|
||||
0x05 => 1024,
|
||||
0x06 => cbor_array![1],
|
||||
0x07 => 20,
|
||||
0x08 => 256,
|
||||
0x09 => cbor_array!["usb"],
|
||||
0x0A => cbor_array![ES256_CRED_PARAM],
|
||||
0x0B => 1024,
|
||||
0x0C => false,
|
||||
0x0D => 4,
|
||||
0x0E => 0,
|
||||
0x0F => 1024,
|
||||
0x10 => 8,
|
||||
0x13 => cbor_map! {"example-cert" => 1},
|
||||
0x14 => 150,
|
||||
};
|
||||
assert_eq!(response_cbor, Some(expected_cbor));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_used_client_pin_into_cbor() {
|
||||
let mut env = TestEnv::default();
|
||||
let sk = crypto::ecdh::SecKey::gensk(env.rng());
|
||||
let pk = sk.genpk();
|
||||
let cose_key = CoseKey::from(pk);
|
||||
let client_pin_response = AuthenticatorClientPinResponse {
|
||||
key_agreement: Some(cose_key.clone()),
|
||||
pin_uv_auth_token: Some(vec![70]),
|
||||
retries: Some(8),
|
||||
power_cycle_state: Some(false),
|
||||
};
|
||||
let response_cbor: Option<cbor::Value> =
|
||||
ResponseData::AuthenticatorClientPin(Some(client_pin_response)).into();
|
||||
let expected_cbor = cbor_map_options! {
|
||||
0x01 => cbor::Value::from(cose_key),
|
||||
0x02 => vec![70],
|
||||
0x03 => 8,
|
||||
0x04 => false,
|
||||
};
|
||||
assert_eq!(response_cbor, Some(expected_cbor));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_client_pin_into_cbor() {
|
||||
let response_cbor: Option<cbor::Value> = ResponseData::AuthenticatorClientPin(None).into();
|
||||
assert_eq!(response_cbor, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reset_into_cbor() {
|
||||
let response_cbor: Option<cbor::Value> = ResponseData::AuthenticatorReset.into();
|
||||
assert_eq!(response_cbor, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_used_credential_management_into_cbor() {
|
||||
let cred_management_response = AuthenticatorCredentialManagementResponse::default();
|
||||
let response_cbor: Option<cbor::Value> =
|
||||
ResponseData::AuthenticatorCredentialManagement(Some(cred_management_response)).into();
|
||||
let expected_cbor = cbor_map_options! {};
|
||||
assert_eq!(response_cbor, Some(expected_cbor));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_used_credential_management_optionals_into_cbor() {
|
||||
let mut env = TestEnv::default();
|
||||
let sk = crypto::ecdh::SecKey::gensk(env.rng());
|
||||
let rp = PublicKeyCredentialRpEntity {
|
||||
rp_id: String::from("example.com"),
|
||||
rp_name: None,
|
||||
rp_icon: None,
|
||||
};
|
||||
let user = PublicKeyCredentialUserEntity {
|
||||
user_id: vec![0xFA, 0xB1, 0xA2],
|
||||
user_name: None,
|
||||
user_display_name: None,
|
||||
user_icon: None,
|
||||
};
|
||||
let cred_descriptor = PublicKeyCredentialDescriptor {
|
||||
key_type: PublicKeyCredentialType::PublicKey,
|
||||
key_id: vec![0x1D; 32],
|
||||
transports: None,
|
||||
};
|
||||
let pk = sk.genpk();
|
||||
let cose_key = CoseKey::from(pk);
|
||||
|
||||
let cred_management_response = AuthenticatorCredentialManagementResponse {
|
||||
existing_resident_credentials_count: Some(100),
|
||||
max_possible_remaining_resident_credentials_count: Some(96),
|
||||
rp: Some(rp.clone()),
|
||||
rp_id_hash: Some(vec![0x1D; 32]),
|
||||
total_rps: Some(3),
|
||||
user: Some(user.clone()),
|
||||
credential_id: Some(cred_descriptor.clone()),
|
||||
public_key: Some(cose_key.clone()),
|
||||
total_credentials: Some(2),
|
||||
cred_protect: Some(CredentialProtectionPolicy::UserVerificationOptional),
|
||||
large_blob_key: Some(vec![0xBB; 64]),
|
||||
};
|
||||
let response_cbor: Option<cbor::Value> =
|
||||
ResponseData::AuthenticatorCredentialManagement(Some(cred_management_response)).into();
|
||||
let expected_cbor = cbor_map_options! {
|
||||
0x01 => 100,
|
||||
0x02 => 96,
|
||||
0x03 => rp,
|
||||
0x04 => vec![0x1D; 32],
|
||||
0x05 => 3,
|
||||
0x06 => user,
|
||||
0x07 => cred_descriptor,
|
||||
0x08 => cbor::Value::from(cose_key),
|
||||
0x09 => 2,
|
||||
0x0A => 0x01,
|
||||
0x0B => vec![0xBB; 64],
|
||||
};
|
||||
assert_eq!(response_cbor, Some(expected_cbor));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_credential_management_into_cbor() {
|
||||
let response_cbor: Option<cbor::Value> =
|
||||
ResponseData::AuthenticatorCredentialManagement(None).into();
|
||||
assert_eq!(response_cbor, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_selection_into_cbor() {
|
||||
let response_cbor: Option<cbor::Value> = ResponseData::AuthenticatorSelection.into();
|
||||
assert_eq!(response_cbor, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_large_blobs_into_cbor() {
|
||||
let large_blobs_response = AuthenticatorLargeBlobsResponse { config: vec![0xC0] };
|
||||
let response_cbor: Option<cbor::Value> =
|
||||
ResponseData::AuthenticatorLargeBlobs(Some(large_blobs_response)).into();
|
||||
let expected_cbor = cbor_map_options! {
|
||||
0x01 => vec![0xC0],
|
||||
};
|
||||
assert_eq!(response_cbor, Some(expected_cbor));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_large_blobs_into_cbor() {
|
||||
let response_cbor: Option<cbor::Value> = ResponseData::AuthenticatorLargeBlobs(None).into();
|
||||
assert_eq!(response_cbor, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_into_cbor() {
|
||||
let response_cbor: Option<cbor::Value> = ResponseData::AuthenticatorConfig.into();
|
||||
assert_eq!(response_cbor, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vendor_response_into_cbor() {
|
||||
let response_cbor: Option<cbor::Value> =
|
||||
ResponseData::AuthenticatorVendorConfigure(AuthenticatorVendorConfigureResponse {
|
||||
cert_programmed: true,
|
||||
pkey_programmed: false,
|
||||
})
|
||||
.into();
|
||||
assert_eq!(
|
||||
response_cbor,
|
||||
Some(cbor_map_options! {
|
||||
0x01 => true,
|
||||
0x02 => false,
|
||||
})
|
||||
);
|
||||
let response_cbor: Option<cbor::Value> =
|
||||
ResponseData::AuthenticatorVendorConfigure(AuthenticatorVendorConfigureResponse {
|
||||
cert_programmed: false,
|
||||
pkey_programmed: true,
|
||||
})
|
||||
.into();
|
||||
assert_eq!(
|
||||
response_cbor,
|
||||
Some(cbor_map_options! {
|
||||
0x01 => false,
|
||||
0x02 => true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vendor_upgrade_into_cbor() {
|
||||
let response_cbor: Option<cbor::Value> = ResponseData::AuthenticatorVendorUpgrade.into();
|
||||
assert_eq!(response_cbor, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vendor_upgrade_info_into_cbor() {
|
||||
let vendor_upgrade_info_response =
|
||||
AuthenticatorVendorUpgradeInfoResponse { info: 0x00060000 };
|
||||
let response_cbor: Option<cbor::Value> =
|
||||
ResponseData::AuthenticatorVendorUpgradeInfo(vendor_upgrade_info_response).into();
|
||||
let expected_cbor = cbor_map! {
|
||||
0x01 => 0x00060000,
|
||||
};
|
||||
assert_eq!(response_cbor, Some(expected_cbor));
|
||||
}
|
||||
}
|
||||
113
libraries/opensk/src/ctap/status_code.rs
Normal file
113
libraries/opensk/src/ctap/status_code.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
// Copyright 2019-2021 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::api::user_presence::UserPresenceError;
|
||||
use crate::api::{attestation_store, key_store};
|
||||
|
||||
// CTAP specification (version 20190130) section 6.3
|
||||
// For now, only the CTAP2 codes are here, the CTAP1 are not included.
|
||||
#[allow(non_camel_case_types)]
|
||||
#[allow(dead_code)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum Ctap2StatusCode {
|
||||
CTAP2_OK = 0x00,
|
||||
CTAP1_ERR_INVALID_COMMAND = 0x01,
|
||||
CTAP1_ERR_INVALID_PARAMETER = 0x02,
|
||||
CTAP1_ERR_INVALID_LENGTH = 0x03,
|
||||
CTAP1_ERR_INVALID_SEQ = 0x04,
|
||||
CTAP1_ERR_TIMEOUT = 0x05,
|
||||
CTAP1_ERR_CHANNEL_BUSY = 0x06,
|
||||
CTAP1_ERR_LOCK_REQUIRED = 0x0A,
|
||||
CTAP1_ERR_INVALID_CHANNEL = 0x0B,
|
||||
CTAP2_ERR_CBOR_UNEXPECTED_TYPE = 0x11,
|
||||
CTAP2_ERR_INVALID_CBOR = 0x12,
|
||||
CTAP2_ERR_MISSING_PARAMETER = 0x14,
|
||||
CTAP2_ERR_LIMIT_EXCEEDED = 0x15,
|
||||
CTAP2_ERR_FP_DATABASE_FULL = 0x17,
|
||||
CTAP2_ERR_LARGE_BLOB_STORAGE_FULL = 0x18,
|
||||
CTAP2_ERR_CREDENTIAL_EXCLUDED = 0x19,
|
||||
CTAP2_ERR_PROCESSING = 0x21,
|
||||
CTAP2_ERR_INVALID_CREDENTIAL = 0x22,
|
||||
CTAP2_ERR_USER_ACTION_PENDING = 0x23,
|
||||
CTAP2_ERR_OPERATION_PENDING = 0x24,
|
||||
CTAP2_ERR_NO_OPERATIONS = 0x25,
|
||||
CTAP2_ERR_UNSUPPORTED_ALGORITHM = 0x26,
|
||||
CTAP2_ERR_OPERATION_DENIED = 0x27,
|
||||
CTAP2_ERR_KEY_STORE_FULL = 0x28,
|
||||
CTAP2_ERR_NO_OPERATION_PENDING = 0x2A,
|
||||
CTAP2_ERR_UNSUPPORTED_OPTION = 0x2B,
|
||||
CTAP2_ERR_INVALID_OPTION = 0x2C,
|
||||
CTAP2_ERR_KEEPALIVE_CANCEL = 0x2D,
|
||||
CTAP2_ERR_NO_CREDENTIALS = 0x2E,
|
||||
CTAP2_ERR_USER_ACTION_TIMEOUT = 0x2F,
|
||||
CTAP2_ERR_NOT_ALLOWED = 0x30,
|
||||
CTAP2_ERR_PIN_INVALID = 0x31,
|
||||
CTAP2_ERR_PIN_BLOCKED = 0x32,
|
||||
CTAP2_ERR_PIN_AUTH_INVALID = 0x33,
|
||||
CTAP2_ERR_PIN_AUTH_BLOCKED = 0x34,
|
||||
CTAP2_ERR_PIN_NOT_SET = 0x35,
|
||||
CTAP2_ERR_PUAT_REQUIRED = 0x36,
|
||||
CTAP2_ERR_PIN_POLICY_VIOLATION = 0x37,
|
||||
CTAP2_ERR_PIN_TOKEN_EXPIRED = 0x38,
|
||||
CTAP2_ERR_REQUEST_TOO_LARGE = 0x39,
|
||||
CTAP2_ERR_ACTION_TIMEOUT = 0x3A,
|
||||
CTAP2_ERR_UP_REQUIRED = 0x3B,
|
||||
CTAP2_ERR_UV_BLOCKED = 0x3C,
|
||||
CTAP2_ERR_INTEGRITY_FAILURE = 0x3D,
|
||||
CTAP2_ERR_INVALID_SUBCOMMAND = 0x3E,
|
||||
CTAP2_ERR_UV_INVALID = 0x3F,
|
||||
CTAP2_ERR_UNAUTHORIZED_PERMISSION = 0x40,
|
||||
CTAP1_ERR_OTHER = 0x7F,
|
||||
_CTAP2_ERR_SPEC_LAST = 0xDF,
|
||||
_CTAP2_ERR_EXTENSION_FIRST = 0xE0,
|
||||
_CTAP2_ERR_EXTENSION_LAST = 0xEF,
|
||||
_CTAP2_ERR_VENDOR_FIRST = 0xF0,
|
||||
/// An internal invariant is broken.
|
||||
///
|
||||
/// This type of error is unexpected and the current state is undefined.
|
||||
CTAP2_ERR_VENDOR_INTERNAL_ERROR = 0xF2,
|
||||
|
||||
/// The hardware is malfunctioning.
|
||||
///
|
||||
/// It may be possible that some of those errors are actually internal errors.
|
||||
CTAP2_ERR_VENDOR_HARDWARE_FAILURE = 0xF3,
|
||||
_CTAP2_ERR_VENDOR_LAST = 0xFF,
|
||||
}
|
||||
|
||||
impl From<UserPresenceError> for Ctap2StatusCode {
|
||||
fn from(user_presence_error: UserPresenceError) -> Self {
|
||||
match user_presence_error {
|
||||
UserPresenceError::Timeout => Self::CTAP2_ERR_USER_ACTION_TIMEOUT,
|
||||
UserPresenceError::Declined => Self::CTAP2_ERR_OPERATION_DENIED,
|
||||
UserPresenceError::Canceled => Self::CTAP2_ERR_KEEPALIVE_CANCEL,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<key_store::Error> for Ctap2StatusCode {
|
||||
fn from(_: key_store::Error) -> Self {
|
||||
Self::CTAP2_ERR_VENDOR_INTERNAL_ERROR
|
||||
}
|
||||
}
|
||||
|
||||
impl From<attestation_store::Error> for Ctap2StatusCode {
|
||||
fn from(error: attestation_store::Error) -> Self {
|
||||
use attestation_store::Error;
|
||||
match error {
|
||||
Error::Storage => Self::CTAP2_ERR_VENDOR_HARDWARE_FAILURE,
|
||||
Error::Internal => Self::CTAP2_ERR_VENDOR_INTERNAL_ERROR,
|
||||
Error::NoSupport => Self::CTAP2_ERR_VENDOR_INTERNAL_ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
1151
libraries/opensk/src/ctap/storage.rs
Normal file
1151
libraries/opensk/src/ctap/storage.rs
Normal file
File diff suppressed because it is too large
Load Diff
161
libraries/opensk/src/ctap/storage/key.rs
Normal file
161
libraries/opensk/src/ctap/storage/key.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
// Copyright 2019-2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/// Number of keys that persist the CTAP reset command.
|
||||
pub const NUM_PERSISTENT_KEYS: usize = 20;
|
||||
|
||||
/// Defines a key given its name and value or range of values.
|
||||
macro_rules! make_key {
|
||||
($(#[$doc: meta])* $name: ident = $key: literal..$end: literal) => {
|
||||
$(#[$doc])* pub const $name: core::ops::Range<usize> = $key..$end;
|
||||
};
|
||||
($(#[$doc: meta])* $name: ident = $key: literal) => {
|
||||
$(#[$doc])* pub const $name: usize = $key;
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the range of values of a key given its value description.
|
||||
#[cfg(test)]
|
||||
macro_rules! make_range {
|
||||
($key: literal..$end: literal) => {
|
||||
$key..$end
|
||||
};
|
||||
($key: literal) => {
|
||||
$key..$key + 1
|
||||
};
|
||||
}
|
||||
|
||||
/// Helper to define keys as a partial partition of a range.
|
||||
macro_rules! make_partition {
|
||||
($range: expr,
|
||||
$(
|
||||
$(#[$doc: meta])*
|
||||
$name: ident = $key: literal $(.. $end: literal)?;
|
||||
)*) => {
|
||||
$(
|
||||
make_key!($(#[$doc])* $name = $key $(.. $end)?);
|
||||
)*
|
||||
#[cfg(test)]
|
||||
const KEY_RANGE: core::ops::Range<usize> = $range;
|
||||
#[cfg(test)]
|
||||
const ALL_KEYS: &[core::ops::Range<usize>] = &[$(make_range!($key $(.. $end)?)),*];
|
||||
};
|
||||
}
|
||||
|
||||
make_partition! {
|
||||
// We reserve 0 and 2048+ for possible migration purposes. We add persistent entries starting
|
||||
// from 1 and going up. We add non-persistent entries starting from 2047 and going down. This
|
||||
// way, we don't commit to a fixed number of persistent keys.
|
||||
1..2048,
|
||||
|
||||
// WARNING: Keys should not be deleted but prefixed with `_` to avoid accidentally reusing them.
|
||||
|
||||
/// Reserved for the attestation store implementation of the environment.
|
||||
_RESERVED_ATTESTATION_STORE = 1..3;
|
||||
|
||||
/// Used for the AAGUID before, but deprecated.
|
||||
_AAGUID = 3;
|
||||
|
||||
// This is the persistent key limit:
|
||||
// - When adding a (persistent) key above this message, make sure its value is smaller than
|
||||
// NUM_PERSISTENT_KEYS.
|
||||
// - When adding a (non-persistent) key below this message, make sure its value is bigger or
|
||||
// equal than NUM_PERSISTENT_KEYS.
|
||||
|
||||
/// Reserved for future credential-related objects.
|
||||
///
|
||||
/// In particular, additional credentials could be added there by reducing the lower bound of
|
||||
/// the credential range below as well as the upper bound of this range in a similar manner.
|
||||
_RESERVED_CREDENTIALS = 1000..1700;
|
||||
|
||||
/// The credentials.
|
||||
///
|
||||
/// Depending on `Customization::max_supported_resident_keys()`, only a prefix of those keys is used.
|
||||
/// Each board may configure `Customization::max_supported_resident_keys()` depending on the
|
||||
/// storage size.
|
||||
CREDENTIALS = 1700..2000;
|
||||
|
||||
/// Storage for the serialized large blob array.
|
||||
///
|
||||
/// The stored large blob can be too big for one key, so it has to be sharded.
|
||||
LARGE_BLOB_SHARDS = 2000..2004;
|
||||
|
||||
/// If this entry exists and is empty, alwaysUv is enabled.
|
||||
ALWAYS_UV = 2038;
|
||||
|
||||
/// If this entry exists and is empty, enterprise attestation is enabled.
|
||||
ENTERPRISE_ATTESTATION = 2039;
|
||||
|
||||
/// If this entry exists and is empty, the PIN needs to be changed.
|
||||
FORCE_PIN_CHANGE = 2040;
|
||||
|
||||
/// The secret of the CredRandom feature.
|
||||
CRED_RANDOM_SECRET = 2041;
|
||||
|
||||
/// List of RP IDs allowed to read the minimum PIN length.
|
||||
MIN_PIN_LENGTH_RP_IDS = 2042;
|
||||
|
||||
/// The minimum PIN length.
|
||||
///
|
||||
/// If the entry is absent, the minimum PIN length is `Customization::default_min_pin_length()`.
|
||||
MIN_PIN_LENGTH = 2043;
|
||||
|
||||
/// The number of PIN retries.
|
||||
///
|
||||
/// If the entry is absent, the number of PIN retries is `Customization::max_pin_retries()`.
|
||||
PIN_RETRIES = 2044;
|
||||
|
||||
/// The PIN hash and length.
|
||||
///
|
||||
/// If the entry is absent, there is no PIN set. The first byte represents
|
||||
/// the length, the following are an array with the hash.
|
||||
PIN_PROPERTIES = 2045;
|
||||
|
||||
/// Reserved for the key store implementation of the environment.
|
||||
_RESERVED_KEY_STORE = 2046;
|
||||
|
||||
/// The global signature counter.
|
||||
///
|
||||
/// If the entry is absent, the counter is 0.
|
||||
GLOBAL_SIGNATURE_COUNTER = 2047;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::api::customization::Customization;
|
||||
use crate::env::test::TestEnv;
|
||||
use crate::env::Env;
|
||||
|
||||
#[test]
|
||||
fn enough_credentials() {
|
||||
let env = TestEnv::default();
|
||||
assert!(
|
||||
env.customization().max_supported_resident_keys()
|
||||
<= CREDENTIALS.end - CREDENTIALS.start
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keys_are_disjoint() {
|
||||
// Check that keys are in the range.
|
||||
for keys in ALL_KEYS {
|
||||
assert!(KEY_RANGE.start <= keys.start && keys.end <= KEY_RANGE.end);
|
||||
}
|
||||
// Check that keys are assigned at most once, essentially partitioning the range.
|
||||
for key in KEY_RANGE {
|
||||
assert!(ALL_KEYS.iter().filter(|keys| keys.contains(&key)).count() <= 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
275
libraries/opensk/src/ctap/token_state.rs
Normal file
275
libraries/opensk/src/ctap/token_state.rs
Normal file
@@ -0,0 +1,275 @@
|
||||
// Copyright 2021 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::api::clock::Clock;
|
||||
use crate::ctap::client_pin::PinPermission;
|
||||
use crate::ctap::status_code::Ctap2StatusCode;
|
||||
use crate::env::Env;
|
||||
use alloc::string::String;
|
||||
use crypto::sha256::Sha256;
|
||||
use crypto::Hash256;
|
||||
|
||||
/// Timeout for auth tokens.
|
||||
///
|
||||
/// This usage time limit is correct for USB, BLE, and internal.
|
||||
/// NFC only allows 19.8 seconds.
|
||||
/// TODO(#15) multiplex over transports, add NFC
|
||||
const INITIAL_USAGE_TIME_LIMIT_MS: usize = 30000;
|
||||
|
||||
/// Implements pinUvAuthToken state from section 6.5.2.1.
|
||||
///
|
||||
/// The userPresent flag is omitted as the only way to set it to true is
|
||||
/// built-in user verification. Therefore, we never cache user presence.
|
||||
///
|
||||
/// This implementation does not use a rolling timer.
|
||||
pub struct PinUvAuthTokenState<E: Env> {
|
||||
// Relies on the fact that all permissions are represented by powers of two.
|
||||
permissions_set: u8,
|
||||
permissions_rp_id: Option<String>,
|
||||
usage_timer: <E::Clock as Clock>::Timer,
|
||||
user_verified: bool,
|
||||
in_use: bool,
|
||||
}
|
||||
|
||||
impl<E: Env> PinUvAuthTokenState<E> {
|
||||
/// Creates a pinUvAuthToken state without permissions.
|
||||
pub fn new() -> Self {
|
||||
PinUvAuthTokenState {
|
||||
permissions_set: 0,
|
||||
permissions_rp_id: None,
|
||||
usage_timer: <E::Clock as Clock>::Timer::default(),
|
||||
user_verified: false,
|
||||
in_use: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the pinUvAuthToken is active.
|
||||
pub fn is_in_use(&self) -> bool {
|
||||
self.in_use
|
||||
}
|
||||
|
||||
/// Checks if the permission is granted.
|
||||
pub fn has_permission(&self, permission: PinPermission) -> Result<(), Ctap2StatusCode> {
|
||||
if permission as u8 & self.permissions_set != 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if there is no associated permissions RPID.
|
||||
pub fn has_no_permissions_rp_id(&self) -> Result<(), Ctap2StatusCode> {
|
||||
if self.permissions_rp_id.is_some() {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks if the permissions RPID is associated.
|
||||
pub fn has_permissions_rp_id(&self, rp_id: &str) -> Result<(), Ctap2StatusCode> {
|
||||
match &self.permissions_rp_id {
|
||||
Some(p) if rp_id == p => Ok(()),
|
||||
_ => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID),
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the permissions RPID's association matches the hash.
|
||||
pub fn has_permissions_rp_id_hash(&self, rp_id_hash: &[u8]) -> Result<(), Ctap2StatusCode> {
|
||||
match &self.permissions_rp_id {
|
||||
Some(p) if rp_id_hash == Sha256::hash(p.as_bytes()) => Ok(()),
|
||||
_ => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the permissions, represented as bits in a byte.
|
||||
pub fn set_permissions(&mut self, permissions: u8) {
|
||||
self.permissions_set = permissions;
|
||||
}
|
||||
|
||||
/// Sets the permissions RPID.
|
||||
pub fn set_permissions_rp_id(&mut self, permissions_rp_id: Option<String>) {
|
||||
self.permissions_rp_id = permissions_rp_id;
|
||||
}
|
||||
|
||||
/// Sets the default permissions.
|
||||
///
|
||||
/// Allows MakeCredential and GetAssertion, without specifying a RP ID.
|
||||
pub fn set_default_permissions(&mut self) {
|
||||
self.set_permissions(0x03);
|
||||
self.set_permissions_rp_id(None);
|
||||
}
|
||||
|
||||
/// Starts the timer for pinUvAuthToken usage.
|
||||
pub fn begin_using_pin_uv_auth_token(&mut self, env: &mut E) {
|
||||
self.user_verified = true;
|
||||
self.usage_timer = env.clock().make_timer(INITIAL_USAGE_TIME_LIMIT_MS);
|
||||
self.in_use = true;
|
||||
}
|
||||
|
||||
/// Updates the usage timer, and disables the pinUvAuthToken on timeout.
|
||||
pub fn pin_uv_auth_token_usage_timer_observer(&mut self, env: &mut E) {
|
||||
if !self.in_use {
|
||||
return;
|
||||
}
|
||||
if env.clock().is_elapsed(&self.usage_timer) {
|
||||
self.stop_using_pin_uv_auth_token();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the user is verified.
|
||||
pub fn get_user_verified_flag_value(&self) -> bool {
|
||||
self.in_use && self.user_verified
|
||||
}
|
||||
|
||||
/// Consumes the user verification.
|
||||
pub fn clear_user_verified_flag(&mut self) {
|
||||
self.user_verified = false;
|
||||
}
|
||||
|
||||
/// Clears all permissions except Large Blob Write.
|
||||
pub fn clear_pin_uv_auth_token_permissions_except_lbw(&mut self) {
|
||||
self.permissions_set &= PinPermission::LargeBlobWrite as u8;
|
||||
}
|
||||
|
||||
/// Resets to the initial state.
|
||||
pub fn stop_using_pin_uv_auth_token(&mut self) {
|
||||
self.permissions_rp_id = None;
|
||||
self.permissions_set = 0;
|
||||
self.usage_timer = <E::Clock as Clock>::Timer::default();
|
||||
self.user_verified = false;
|
||||
self.in_use = false;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::env::test::TestEnv;
|
||||
use enum_iterator::IntoEnumIterator;
|
||||
|
||||
#[test]
|
||||
fn test_observer() {
|
||||
let mut env = TestEnv::default();
|
||||
let mut token_state = PinUvAuthTokenState::<TestEnv>::new();
|
||||
token_state.begin_using_pin_uv_auth_token(&mut env);
|
||||
assert!(token_state.is_in_use());
|
||||
env.clock().advance(100);
|
||||
token_state.pin_uv_auth_token_usage_timer_observer(&mut env);
|
||||
assert!(token_state.is_in_use());
|
||||
env.clock().advance(INITIAL_USAGE_TIME_LIMIT_MS);
|
||||
token_state.pin_uv_auth_token_usage_timer_observer(&mut env);
|
||||
assert!(!token_state.is_in_use());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stop() {
|
||||
let mut env = TestEnv::default();
|
||||
let mut token_state = PinUvAuthTokenState::<TestEnv>::new();
|
||||
token_state.begin_using_pin_uv_auth_token(&mut env);
|
||||
assert!(token_state.is_in_use());
|
||||
token_state.stop_using_pin_uv_auth_token();
|
||||
assert!(!token_state.is_in_use());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permissions() {
|
||||
let mut token_state = PinUvAuthTokenState::<TestEnv>::new();
|
||||
token_state.set_permissions(0xFF);
|
||||
for permission in PinPermission::into_enum_iter() {
|
||||
assert_eq!(token_state.has_permission(permission), Ok(()));
|
||||
}
|
||||
token_state.clear_pin_uv_auth_token_permissions_except_lbw();
|
||||
assert_eq!(
|
||||
token_state.has_permission(PinPermission::CredentialManagement),
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
|
||||
);
|
||||
assert_eq!(
|
||||
token_state.has_permission(PinPermission::LargeBlobWrite),
|
||||
Ok(())
|
||||
);
|
||||
token_state.stop_using_pin_uv_auth_token();
|
||||
for permission in PinPermission::into_enum_iter() {
|
||||
assert_eq!(
|
||||
token_state.has_permission(permission),
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permissions_rp_id_none() {
|
||||
let mut token_state = PinUvAuthTokenState::<TestEnv>::new();
|
||||
let example_hash = Sha256::hash(b"example.com");
|
||||
token_state.set_permissions_rp_id(None);
|
||||
assert_eq!(token_state.has_no_permissions_rp_id(), Ok(()));
|
||||
assert_eq!(
|
||||
token_state.has_permissions_rp_id("example.com"),
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
|
||||
);
|
||||
assert_eq!(
|
||||
token_state.has_permissions_rp_id_hash(&example_hash),
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permissions_rp_id_some() {
|
||||
let mut token_state = PinUvAuthTokenState::<TestEnv>::new();
|
||||
let example_hash = Sha256::hash(b"example.com");
|
||||
token_state.set_permissions_rp_id(Some(String::from("example.com")));
|
||||
|
||||
assert_eq!(
|
||||
token_state.has_no_permissions_rp_id(),
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
|
||||
);
|
||||
assert_eq!(token_state.has_permissions_rp_id("example.com"), Ok(()));
|
||||
assert_eq!(
|
||||
token_state.has_permissions_rp_id("another.example.com"),
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
|
||||
);
|
||||
assert_eq!(
|
||||
token_state.has_permissions_rp_id_hash(&example_hash),
|
||||
Ok(())
|
||||
);
|
||||
assert_eq!(
|
||||
token_state.has_permissions_rp_id_hash(&[0x1D; 32]),
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
|
||||
);
|
||||
|
||||
token_state.stop_using_pin_uv_auth_token();
|
||||
assert_eq!(
|
||||
token_state.has_permissions_rp_id("example.com"),
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
|
||||
);
|
||||
assert_eq!(
|
||||
token_state.has_permissions_rp_id_hash(&example_hash),
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_user_verified_flag() {
|
||||
let mut env = TestEnv::default();
|
||||
let mut token_state = PinUvAuthTokenState::<TestEnv>::new();
|
||||
assert!(!token_state.get_user_verified_flag_value());
|
||||
token_state.begin_using_pin_uv_auth_token(&mut env);
|
||||
assert!(token_state.get_user_verified_flag_value());
|
||||
token_state.clear_user_verified_flag();
|
||||
assert!(!token_state.get_user_verified_flag_value());
|
||||
token_state.begin_using_pin_uv_auth_token(&mut env);
|
||||
assert!(token_state.get_user_verified_flag_value());
|
||||
token_state.stop_using_pin_uv_auth_token();
|
||||
assert!(!token_state.get_user_verified_flag_value());
|
||||
}
|
||||
}
|
||||
137
libraries/opensk/src/ctap/u2f_up.rs
Normal file
137
libraries/opensk/src/ctap/u2f_up.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
// Copyright 2019-2021 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::TOUCH_TIMEOUT_MS;
|
||||
use crate::api::clock::Clock;
|
||||
use crate::env::Env;
|
||||
|
||||
const U2F_UP_PROMPT_TIMEOUT_MS: usize = 10000;
|
||||
|
||||
pub struct U2fUserPresenceState<E: Env> {
|
||||
/// If user presence was recently requested, its timeout is saved here.
|
||||
needs_up: <E::Clock as Clock>::Timer,
|
||||
|
||||
/// Button touch timeouts, while user presence is requested, are saved here.
|
||||
has_up: <E::Clock as Clock>::Timer,
|
||||
}
|
||||
|
||||
impl<E: Env> U2fUserPresenceState<E> {
|
||||
pub fn new() -> U2fUserPresenceState<E> {
|
||||
U2fUserPresenceState {
|
||||
needs_up: <E::Clock as Clock>::Timer::default(),
|
||||
has_up: <E::Clock as Clock>::Timer::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows consuming user presence until timeout, if it was needed.
|
||||
///
|
||||
/// If user presence was not requested, granting user presence does nothing.
|
||||
pub fn grant_up(&mut self, env: &mut E) {
|
||||
if !env.clock().is_elapsed(&self.needs_up) {
|
||||
self.needs_up = <E::Clock as Clock>::Timer::default();
|
||||
self.has_up = env.clock().make_timer(TOUCH_TIMEOUT_MS);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether user presence was granted within the timeout and not yet consumed.
|
||||
pub fn consume_up(&mut self, env: &mut E) -> bool {
|
||||
if !env.clock().is_elapsed(&self.has_up) {
|
||||
self.has_up = <E::Clock as Clock>::Timer::default();
|
||||
true
|
||||
} else {
|
||||
self.needs_up = env.clock().make_timer(U2F_UP_PROMPT_TIMEOUT_MS);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether user presence was requested.
|
||||
///
|
||||
/// This function doesn't represent interaction with the environment, and does not change the
|
||||
/// state, i.e. neither grants nor consumes user presence.
|
||||
pub fn is_up_needed(&mut self, env: &mut E) -> bool {
|
||||
!env.clock().is_elapsed(&self.needs_up)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "with_ctap1")]
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::env::test::TestEnv;
|
||||
|
||||
fn big_positive() -> usize {
|
||||
1000000
|
||||
}
|
||||
|
||||
fn grant_up_when_needed(env: &mut TestEnv) {
|
||||
let mut u2f_state = U2fUserPresenceState::new();
|
||||
assert!(!u2f_state.consume_up(env));
|
||||
assert!(u2f_state.is_up_needed(env));
|
||||
u2f_state.grant_up(env);
|
||||
assert!(u2f_state.consume_up(env));
|
||||
assert!(!u2f_state.consume_up(env));
|
||||
}
|
||||
|
||||
fn need_up_timeout(env: &mut TestEnv) {
|
||||
let mut u2f_state = U2fUserPresenceState::new();
|
||||
assert!(!u2f_state.consume_up(env));
|
||||
assert!(u2f_state.is_up_needed(env));
|
||||
env.clock().advance(U2F_UP_PROMPT_TIMEOUT_MS);
|
||||
// The timeout excludes equality, so it should be over at this instant.
|
||||
assert!(!u2f_state.is_up_needed(env));
|
||||
}
|
||||
|
||||
fn grant_up_timeout(env: &mut TestEnv) {
|
||||
let mut u2f_state = U2fUserPresenceState::new();
|
||||
assert!(!u2f_state.consume_up(env));
|
||||
assert!(u2f_state.is_up_needed(env));
|
||||
u2f_state.grant_up(env);
|
||||
env.clock().advance(TOUCH_TIMEOUT_MS);
|
||||
// The timeout excludes equality, so it should be over at this instant.
|
||||
assert!(!u2f_state.consume_up(env));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_grant_up_timeout() {
|
||||
let mut env = TestEnv::default();
|
||||
grant_up_timeout(&mut env);
|
||||
env.clock().advance(big_positive());
|
||||
grant_up_timeout(&mut env);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_need_up_timeout() {
|
||||
let mut env = TestEnv::default();
|
||||
need_up_timeout(&mut env);
|
||||
env.clock().advance(big_positive());
|
||||
need_up_timeout(&mut env);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_grant_up_when_needed() {
|
||||
let mut env = TestEnv::default();
|
||||
grant_up_when_needed(&mut env);
|
||||
env.clock().advance(big_positive());
|
||||
grant_up_when_needed(&mut env);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_grant_up_without_need() {
|
||||
let mut env = TestEnv::default();
|
||||
let mut u2f_state = U2fUserPresenceState::new();
|
||||
u2f_state.grant_up(&mut env);
|
||||
assert!(!u2f_state.is_up_needed(&mut env));
|
||||
assert!(!u2f_state.consume_up(&mut env));
|
||||
}
|
||||
}
|
||||
153
libraries/opensk/src/ctap/vendor_hid.rs
Normal file
153
libraries/opensk/src/ctap/vendor_hid.rs
Normal file
@@ -0,0 +1,153 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::ctap::hid::{
|
||||
CtapHid, CtapHidCommand, CtapHidError, HidPacket, HidPacketIterator, Message,
|
||||
};
|
||||
use crate::ctap::{Channel, CtapState};
|
||||
use crate::env::Env;
|
||||
|
||||
/// Implements the non-standard command processing for HID.
|
||||
///
|
||||
/// Outside of the pure HID commands like INIT, only PING and CBOR commands are allowed.
|
||||
pub struct VendorHid<E: Env> {
|
||||
hid: CtapHid<E>,
|
||||
}
|
||||
|
||||
impl<E: Env> Default for VendorHid<E> {
|
||||
/// Instantiates a HID handler for CTAP1, CTAP2 and Wink.
|
||||
fn default() -> Self {
|
||||
let hid = CtapHid::<E>::new(CtapHid::<E>::CAPABILITY_CBOR | CtapHid::<E>::CAPABILITY_NMSG);
|
||||
VendorHid { hid }
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Env> VendorHid<E> {
|
||||
/// Processes an incoming USB HID packet, and returns an iterator for all outgoing packets.
|
||||
pub fn process_hid_packet(
|
||||
&mut self,
|
||||
env: &mut E,
|
||||
packet: &HidPacket,
|
||||
ctap_state: &mut CtapState<E>,
|
||||
) -> HidPacketIterator {
|
||||
if let Some(message) = self.hid.parse_packet(env, packet) {
|
||||
let processed_message = self.process_message(env, message, ctap_state);
|
||||
debug_ctap!(
|
||||
env,
|
||||
"Sending message through the second usage page: {:02x?}",
|
||||
processed_message
|
||||
);
|
||||
CtapHid::<E>::split_message(processed_message)
|
||||
} else {
|
||||
HidPacketIterator::none()
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes a message's commands that affect the protocol outside HID.
|
||||
pub fn process_message(
|
||||
&mut self,
|
||||
env: &mut E,
|
||||
message: Message,
|
||||
ctap_state: &mut CtapState<E>,
|
||||
) -> Message {
|
||||
let cid = message.cid;
|
||||
match message.cmd {
|
||||
// There are no custom CTAP1 commands.
|
||||
CtapHidCommand::Msg => CtapHid::<E>::error_message(cid, CtapHidError::InvalidCmd),
|
||||
// The CTAP2 processing function multiplexes internally.
|
||||
CtapHidCommand::Cbor => {
|
||||
let response =
|
||||
ctap_state.process_command(env, &message.payload, Channel::VendorHid(cid));
|
||||
Message {
|
||||
cid,
|
||||
cmd: CtapHidCommand::Cbor,
|
||||
payload: response,
|
||||
}
|
||||
}
|
||||
// Call Wink over the main HID.
|
||||
CtapHidCommand::Wink => CtapHid::<E>::error_message(cid, CtapHidError::InvalidCmd),
|
||||
// All other commands have already been processed, keep them as is.
|
||||
_ => message,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::ctap::hid::ChannelID;
|
||||
use crate::env::test::TestEnv;
|
||||
|
||||
fn new_initialized() -> (VendorHid<TestEnv>, ChannelID) {
|
||||
let (hid, cid) = CtapHid::new_initialized();
|
||||
(VendorHid::<TestEnv> { hid }, cid)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_hid_packet() {
|
||||
let mut env = TestEnv::default();
|
||||
let mut ctap_state = CtapState::<TestEnv>::new(&mut env);
|
||||
let (mut vendor_hid, cid) = new_initialized();
|
||||
|
||||
let mut ping_packet = [0x00; 64];
|
||||
ping_packet[..4].copy_from_slice(&cid);
|
||||
ping_packet[4..9].copy_from_slice(&[0x81, 0x00, 0x02, 0x99, 0x99]);
|
||||
|
||||
let mut response = vendor_hid.process_hid_packet(&mut env, &ping_packet, &mut ctap_state);
|
||||
assert_eq!(response.next(), Some(ping_packet));
|
||||
assert_eq!(response.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_hid_packet_empty() {
|
||||
let mut env = TestEnv::default();
|
||||
let mut ctap_state = CtapState::<TestEnv>::new(&mut env);
|
||||
let (mut vendor_hid, cid) = new_initialized();
|
||||
|
||||
let mut cancel_packet = [0x00; 64];
|
||||
cancel_packet[..4].copy_from_slice(&cid);
|
||||
cancel_packet[4..7].copy_from_slice(&[0x91, 0x00, 0x00]);
|
||||
|
||||
let mut response = vendor_hid.process_hid_packet(&mut env, &cancel_packet, &mut ctap_state);
|
||||
assert_eq!(response.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blocked_commands() {
|
||||
let mut env = TestEnv::default();
|
||||
let mut ctap_state = CtapState::<TestEnv>::new(&mut env);
|
||||
let (mut vendor_hid, cid) = new_initialized();
|
||||
|
||||
// Usually longer, but we don't parse them anyway.
|
||||
let mut msg_packet = [0x00; 64];
|
||||
msg_packet[..4].copy_from_slice(&cid);
|
||||
msg_packet[4..7].copy_from_slice(&[0x83, 0x00, 0x00]);
|
||||
|
||||
let mut wink_packet = [0x00; 64];
|
||||
wink_packet[..4].copy_from_slice(&cid);
|
||||
wink_packet[4..7].copy_from_slice(&[0x88, 0x00, 0x00]);
|
||||
|
||||
let mut error_packet = [0x00; 64];
|
||||
error_packet[..4].copy_from_slice(&cid);
|
||||
error_packet[4..8].copy_from_slice(&[0xBF, 0x00, 0x01, 0x01]);
|
||||
|
||||
let mut response = vendor_hid.process_hid_packet(&mut env, &msg_packet, &mut ctap_state);
|
||||
assert_eq!(response.next(), Some(error_packet));
|
||||
assert_eq!(response.next(), None);
|
||||
|
||||
let mut response = vendor_hid.process_hid_packet(&mut env, &wink_packet, &mut ctap_state);
|
||||
assert_eq!(response.next(), Some(error_packet));
|
||||
assert_eq!(response.next(), None);
|
||||
}
|
||||
}
|
||||
74
libraries/opensk/src/env/mod.rs
vendored
Normal file
74
libraries/opensk/src/env/mod.rs
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright 2022-2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::api::attestation_store::AttestationStore;
|
||||
use crate::api::clock::Clock;
|
||||
use crate::api::connection::HidConnection;
|
||||
use crate::api::customization::Customization;
|
||||
use crate::api::firmware_protection::FirmwareProtection;
|
||||
use crate::api::key_store::KeyStore;
|
||||
use crate::api::upgrade_storage::UpgradeStorage;
|
||||
use crate::api::user_presence::UserPresence;
|
||||
use persistent_store::{Storage, Store};
|
||||
use rng256::Rng256;
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
pub mod test;
|
||||
|
||||
/// Describes what CTAP needs to function.
|
||||
pub trait Env {
|
||||
type Rng: Rng256;
|
||||
type UserPresence: UserPresence;
|
||||
type Storage: Storage;
|
||||
type KeyStore: KeyStore;
|
||||
type UpgradeStorage: UpgradeStorage;
|
||||
type FirmwareProtection: FirmwareProtection;
|
||||
type Write: core::fmt::Write;
|
||||
type Customization: Customization;
|
||||
type HidConnection: HidConnection;
|
||||
type AttestationStore: AttestationStore;
|
||||
type Clock: Clock;
|
||||
|
||||
fn rng(&mut self) -> &mut Self::Rng;
|
||||
fn user_presence(&mut self) -> &mut Self::UserPresence;
|
||||
fn store(&mut self) -> &mut Store<Self::Storage>;
|
||||
fn key_store(&mut self) -> &mut Self::KeyStore;
|
||||
fn attestation_store(&mut self) -> &mut Self::AttestationStore;
|
||||
fn clock(&mut self) -> &mut Self::Clock;
|
||||
|
||||
/// Returns the upgrade storage instance.
|
||||
///
|
||||
/// Upgrade storage is optional, so implementations may return `None`. However, implementations
|
||||
/// should either always return `None` or always return `Some`.
|
||||
fn upgrade_storage(&mut self) -> Option<&mut Self::UpgradeStorage>;
|
||||
|
||||
fn firmware_protection(&mut self) -> &mut Self::FirmwareProtection;
|
||||
|
||||
/// Creates a write instance for debugging.
|
||||
///
|
||||
/// This API doesn't return a reference such that drop may flush. This matches the Tock
|
||||
/// environment. Non-Tock embedded environments should use the defmt feature (to be implemented
|
||||
/// using the defmt crate) and ignore this API. Non-embedded environments may either use this
|
||||
/// API or use the log feature (to be implemented using the log crate).
|
||||
fn write(&mut self) -> Self::Write;
|
||||
|
||||
fn customization(&self) -> &Self::Customization;
|
||||
|
||||
/// I/O connection for sending packets implementing CTAP HID protocol.
|
||||
fn main_hid_connection(&mut self) -> &mut Self::HidConnection;
|
||||
|
||||
/// I/O connection for sending packets implementing vendor extensions to CTAP HID protocol.
|
||||
#[cfg(feature = "vendor_hid")]
|
||||
fn vendor_hid_connection(&mut self) -> &mut Self::HidConnection;
|
||||
}
|
||||
195
libraries/opensk/src/env/test/customization.rs
vendored
Normal file
195
libraries/opensk/src/env/test/customization.rs
vendored
Normal file
@@ -0,0 +1,195 @@
|
||||
// Copyright 2022-2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::api::customization::{Customization, CustomizationImpl, AAGUID_LENGTH};
|
||||
use crate::ctap::data_formats::{CredentialProtectionPolicy, EnterpriseAttestationMode};
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
pub struct TestCustomization {
|
||||
aaguid: &'static [u8; AAGUID_LENGTH],
|
||||
allows_pin_protocol_v1: bool,
|
||||
default_cred_protect: Option<CredentialProtectionPolicy>,
|
||||
default_min_pin_length: u8,
|
||||
default_min_pin_length_rp_ids: Vec<String>,
|
||||
enforce_always_uv: bool,
|
||||
enterprise_attestation_mode: Option<EnterpriseAttestationMode>,
|
||||
enterprise_rp_id_list: Vec<String>,
|
||||
max_msg_size: usize,
|
||||
max_pin_retries: u8,
|
||||
use_batch_attestation: bool,
|
||||
use_signature_counter: bool,
|
||||
max_cred_blob_length: usize,
|
||||
max_credential_count_in_list: Option<usize>,
|
||||
max_large_blob_array_size: usize,
|
||||
max_rp_ids_length: usize,
|
||||
max_supported_resident_keys: usize,
|
||||
}
|
||||
|
||||
impl TestCustomization {
|
||||
pub fn set_allows_pin_protocol_v1(&mut self, is_allowed: bool) {
|
||||
self.allows_pin_protocol_v1 = is_allowed;
|
||||
}
|
||||
|
||||
pub fn setup_enterprise_attestation(
|
||||
&mut self,
|
||||
mode: Option<EnterpriseAttestationMode>,
|
||||
rp_id_list: Option<Vec<String>>,
|
||||
) {
|
||||
self.enterprise_attestation_mode = mode;
|
||||
if let Some(rp_id_list) = rp_id_list {
|
||||
self.enterprise_rp_id_list = rp_id_list;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Customization for TestCustomization {
|
||||
fn aaguid(&self) -> &'static [u8; AAGUID_LENGTH] {
|
||||
self.aaguid
|
||||
}
|
||||
|
||||
fn allows_pin_protocol_v1(&self) -> bool {
|
||||
self.allows_pin_protocol_v1
|
||||
}
|
||||
|
||||
fn default_cred_protect(&self) -> Option<CredentialProtectionPolicy> {
|
||||
self.default_cred_protect
|
||||
}
|
||||
|
||||
fn default_min_pin_length(&self) -> u8 {
|
||||
self.default_min_pin_length
|
||||
}
|
||||
|
||||
fn default_min_pin_length_rp_ids(&self) -> Vec<String> {
|
||||
self.default_min_pin_length_rp_ids.clone()
|
||||
}
|
||||
|
||||
fn enforce_always_uv(&self) -> bool {
|
||||
self.enforce_always_uv
|
||||
}
|
||||
|
||||
fn enterprise_attestation_mode(&self) -> Option<EnterpriseAttestationMode> {
|
||||
self.enterprise_attestation_mode
|
||||
}
|
||||
|
||||
fn enterprise_rp_id_list(&self) -> Vec<String> {
|
||||
self.enterprise_rp_id_list.clone()
|
||||
}
|
||||
|
||||
fn is_enterprise_rp_id(&self, rp_id: &str) -> bool {
|
||||
self.enterprise_rp_id_list.iter().any(|id| id == rp_id)
|
||||
}
|
||||
|
||||
fn max_msg_size(&self) -> usize {
|
||||
self.max_msg_size
|
||||
}
|
||||
|
||||
fn max_pin_retries(&self) -> u8 {
|
||||
self.max_pin_retries
|
||||
}
|
||||
|
||||
fn use_batch_attestation(&self) -> bool {
|
||||
self.use_batch_attestation
|
||||
}
|
||||
|
||||
fn use_signature_counter(&self) -> bool {
|
||||
self.use_signature_counter
|
||||
}
|
||||
|
||||
fn max_cred_blob_length(&self) -> usize {
|
||||
self.max_cred_blob_length
|
||||
}
|
||||
|
||||
fn max_credential_count_in_list(&self) -> Option<usize> {
|
||||
self.max_credential_count_in_list
|
||||
}
|
||||
|
||||
fn max_large_blob_array_size(&self) -> usize {
|
||||
self.max_large_blob_array_size
|
||||
}
|
||||
|
||||
fn max_rp_ids_length(&self) -> usize {
|
||||
self.max_rp_ids_length
|
||||
}
|
||||
|
||||
fn max_supported_resident_keys(&self) -> usize {
|
||||
self.max_supported_resident_keys
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CustomizationImpl> for TestCustomization {
|
||||
fn from(c: CustomizationImpl) -> Self {
|
||||
let CustomizationImpl {
|
||||
aaguid,
|
||||
allows_pin_protocol_v1,
|
||||
default_cred_protect,
|
||||
default_min_pin_length,
|
||||
default_min_pin_length_rp_ids,
|
||||
enforce_always_uv,
|
||||
enterprise_attestation_mode,
|
||||
enterprise_rp_id_list,
|
||||
max_msg_size,
|
||||
max_pin_retries,
|
||||
use_batch_attestation,
|
||||
use_signature_counter,
|
||||
max_cred_blob_length,
|
||||
max_credential_count_in_list,
|
||||
max_large_blob_array_size,
|
||||
max_rp_ids_length,
|
||||
max_supported_resident_keys,
|
||||
} = c;
|
||||
|
||||
let default_min_pin_length_rp_ids = default_min_pin_length_rp_ids
|
||||
.iter()
|
||||
.map(|s| String::from(*s))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let enterprise_rp_id_list = enterprise_rp_id_list
|
||||
.iter()
|
||||
.map(|s| String::from(*s))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Self {
|
||||
aaguid,
|
||||
allows_pin_protocol_v1,
|
||||
default_cred_protect,
|
||||
default_min_pin_length,
|
||||
default_min_pin_length_rp_ids,
|
||||
enforce_always_uv,
|
||||
enterprise_attestation_mode,
|
||||
enterprise_rp_id_list,
|
||||
max_msg_size,
|
||||
max_pin_retries,
|
||||
use_batch_attestation,
|
||||
use_signature_counter,
|
||||
max_cred_blob_length,
|
||||
max_credential_count_in_list,
|
||||
max_large_blob_array_size,
|
||||
max_rp_ids_length,
|
||||
max_supported_resident_keys,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::api::customization::{is_valid, DEFAULT_CUSTOMIZATION};
|
||||
|
||||
#[test]
|
||||
fn test_invariants() {
|
||||
let customization = TestCustomization::from(DEFAULT_CUSTOMIZATION.clone());
|
||||
assert!(is_valid(&customization));
|
||||
}
|
||||
}
|
||||
286
libraries/opensk/src/env/test/mod.rs
vendored
Normal file
286
libraries/opensk/src/env/test/mod.rs
vendored
Normal file
@@ -0,0 +1,286 @@
|
||||
// Copyright 2022-2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use self::upgrade_storage::BufferUpgradeStorage;
|
||||
use crate::api::attestation_store::AttestationStore;
|
||||
use crate::api::clock::Clock;
|
||||
use crate::api::connection::{HidConnection, SendOrRecvResult, SendOrRecvStatus};
|
||||
use crate::api::customization::DEFAULT_CUSTOMIZATION;
|
||||
use crate::api::firmware_protection::FirmwareProtection;
|
||||
use crate::api::user_presence::{UserPresence, UserPresenceResult};
|
||||
use crate::api::{attestation_store, key_store};
|
||||
use crate::env::Env;
|
||||
use customization::TestCustomization;
|
||||
use persistent_store::{BufferOptions, BufferStorage, Store};
|
||||
use rand::rngs::StdRng;
|
||||
use rand::{Rng, SeedableRng};
|
||||
use rng256::Rng256;
|
||||
|
||||
pub mod customization;
|
||||
mod upgrade_storage;
|
||||
|
||||
pub struct TestEnv {
|
||||
rng: TestRng256,
|
||||
user_presence: TestUserPresence,
|
||||
store: Store<BufferStorage>,
|
||||
upgrade_storage: Option<BufferUpgradeStorage>,
|
||||
customization: TestCustomization,
|
||||
clock: TestClock,
|
||||
}
|
||||
|
||||
pub struct TestRng256 {
|
||||
rng: StdRng,
|
||||
}
|
||||
|
||||
impl TestRng256 {
|
||||
pub fn seed_from_u64(&mut self, state: u64) {
|
||||
self.rng = StdRng::seed_from_u64(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Rng256 for TestRng256 {
|
||||
fn gen_uniform_u8x32(&mut self) -> [u8; 32] {
|
||||
let mut result = [Default::default(); 32];
|
||||
self.rng.fill(&mut result);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct TestTimer {
|
||||
end_ms: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct TestClock {
|
||||
/// The current time, as advanced, in milliseconds.
|
||||
now_ms: usize,
|
||||
}
|
||||
|
||||
impl TestClock {
|
||||
pub fn advance(&mut self, milliseconds: usize) {
|
||||
self.now_ms += milliseconds;
|
||||
}
|
||||
}
|
||||
|
||||
impl Clock for TestClock {
|
||||
type Timer = TestTimer;
|
||||
|
||||
fn make_timer(&mut self, milliseconds: usize) -> Self::Timer {
|
||||
TestTimer {
|
||||
end_ms: self.now_ms + milliseconds,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_elapsed(&mut self, timer: &Self::Timer) -> bool {
|
||||
self.now_ms >= timer.end_ms
|
||||
}
|
||||
|
||||
#[cfg(feature = "debug_ctap")]
|
||||
fn timestamp_us(&mut self) -> usize {
|
||||
// Unused, but let's implement something because it's easy.
|
||||
self.now_ms * 1000
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TestUserPresence {
|
||||
check: Box<dyn Fn() -> UserPresenceResult>,
|
||||
}
|
||||
|
||||
pub struct TestWrite;
|
||||
|
||||
impl core::fmt::Write for TestWrite {
|
||||
fn write_str(&mut self, _: &str) -> core::fmt::Result {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn new_storage() -> BufferStorage {
|
||||
// Use the Nordic configuration.
|
||||
const PAGE_SIZE: usize = 0x1000;
|
||||
const NUM_PAGES: usize = 20;
|
||||
let store = vec![0xff; NUM_PAGES * PAGE_SIZE].into_boxed_slice();
|
||||
let options = BufferOptions {
|
||||
word_size: 4,
|
||||
page_size: PAGE_SIZE,
|
||||
max_word_writes: 2,
|
||||
max_page_erases: 10000,
|
||||
strict_mode: true,
|
||||
};
|
||||
BufferStorage::new(store, options)
|
||||
}
|
||||
|
||||
impl HidConnection for TestEnv {
|
||||
fn send_and_maybe_recv(&mut self, _buf: &mut [u8; 64], _timeout_ms: usize) -> SendOrRecvResult {
|
||||
// TODO: Implement I/O from canned requests/responses for integration testing.
|
||||
Ok(SendOrRecvStatus::Sent)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TestEnv {
|
||||
fn default() -> Self {
|
||||
let rng = TestRng256 {
|
||||
rng: StdRng::seed_from_u64(0),
|
||||
};
|
||||
let user_presence = TestUserPresence {
|
||||
check: Box::new(|| Ok(())),
|
||||
};
|
||||
let storage = new_storage();
|
||||
let store = Store::new(storage).ok().unwrap();
|
||||
let upgrade_storage = Some(BufferUpgradeStorage::new().unwrap());
|
||||
let customization = DEFAULT_CUSTOMIZATION.into();
|
||||
let clock = TestClock::default();
|
||||
TestEnv {
|
||||
rng,
|
||||
user_presence,
|
||||
store,
|
||||
upgrade_storage,
|
||||
customization,
|
||||
clock,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestEnv {
|
||||
pub fn disable_upgrade_storage(&mut self) {
|
||||
self.upgrade_storage = None;
|
||||
}
|
||||
|
||||
pub fn customization_mut(&mut self) -> &mut TestCustomization {
|
||||
&mut self.customization
|
||||
}
|
||||
|
||||
pub fn rng(&mut self) -> &mut TestRng256 {
|
||||
&mut self.rng
|
||||
}
|
||||
}
|
||||
|
||||
impl TestUserPresence {
|
||||
pub fn set(&mut self, check: impl Fn() -> UserPresenceResult + 'static) {
|
||||
self.check = Box::new(check);
|
||||
}
|
||||
}
|
||||
|
||||
impl UserPresence for TestUserPresence {
|
||||
fn check_init(&mut self) {}
|
||||
fn wait_with_timeout(&mut self, _timeout_ms: usize) -> UserPresenceResult {
|
||||
(self.check)()
|
||||
}
|
||||
fn check_complete(&mut self) {}
|
||||
}
|
||||
|
||||
impl FirmwareProtection for TestEnv {
|
||||
fn lock(&mut self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl key_store::Helper for TestEnv {}
|
||||
|
||||
impl AttestationStore for TestEnv {
|
||||
fn get(
|
||||
&mut self,
|
||||
_id: &attestation_store::Id,
|
||||
) -> Result<Option<attestation_store::Attestation>, attestation_store::Error> {
|
||||
attestation_store::helper_get(self)
|
||||
}
|
||||
|
||||
fn set(
|
||||
&mut self,
|
||||
_id: &attestation_store::Id,
|
||||
attestation: Option<&attestation_store::Attestation>,
|
||||
) -> Result<(), attestation_store::Error> {
|
||||
attestation_store::helper_set(self, attestation)
|
||||
}
|
||||
}
|
||||
|
||||
impl Env for TestEnv {
|
||||
type Rng = TestRng256;
|
||||
type UserPresence = TestUserPresence;
|
||||
type Storage = BufferStorage;
|
||||
type KeyStore = Self;
|
||||
type AttestationStore = Self;
|
||||
type Clock = TestClock;
|
||||
type UpgradeStorage = BufferUpgradeStorage;
|
||||
type FirmwareProtection = Self;
|
||||
type Write = TestWrite;
|
||||
type Customization = TestCustomization;
|
||||
type HidConnection = Self;
|
||||
|
||||
fn rng(&mut self) -> &mut Self::Rng {
|
||||
&mut self.rng
|
||||
}
|
||||
|
||||
fn user_presence(&mut self) -> &mut Self::UserPresence {
|
||||
&mut self.user_presence
|
||||
}
|
||||
|
||||
fn store(&mut self) -> &mut Store<Self::Storage> {
|
||||
&mut self.store
|
||||
}
|
||||
|
||||
fn key_store(&mut self) -> &mut Self {
|
||||
self
|
||||
}
|
||||
|
||||
fn attestation_store(&mut self) -> &mut Self {
|
||||
self
|
||||
}
|
||||
|
||||
fn clock(&mut self) -> &mut Self::Clock {
|
||||
&mut self.clock
|
||||
}
|
||||
|
||||
fn upgrade_storage(&mut self) -> Option<&mut Self::UpgradeStorage> {
|
||||
self.upgrade_storage.as_mut()
|
||||
}
|
||||
|
||||
fn firmware_protection(&mut self) -> &mut Self::FirmwareProtection {
|
||||
self
|
||||
}
|
||||
|
||||
fn write(&mut self) -> Self::Write {
|
||||
TestWrite
|
||||
}
|
||||
|
||||
fn customization(&self) -> &Self::Customization {
|
||||
&self.customization
|
||||
}
|
||||
|
||||
fn main_hid_connection(&mut self) -> &mut Self::HidConnection {
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "vendor_hid")]
|
||||
fn vendor_hid_connection(&mut self) -> &mut Self::HidConnection {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::module_inception)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_clock() {
|
||||
let mut clock = TestClock::default();
|
||||
let timer = clock.make_timer(3);
|
||||
assert!(!clock.is_elapsed(&timer));
|
||||
clock.advance(2);
|
||||
assert!(!clock.is_elapsed(&timer));
|
||||
clock.advance(1);
|
||||
assert!(clock.is_elapsed(&timer));
|
||||
}
|
||||
}
|
||||
117
libraries/opensk/src/env/test/upgrade_storage.rs
vendored
Normal file
117
libraries/opensk/src/env/test/upgrade_storage.rs
vendored
Normal file
@@ -0,0 +1,117 @@
|
||||
// Copyright 2021-2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::api::upgrade_storage::helper::ModRange;
|
||||
use crate::api::upgrade_storage::UpgradeStorage;
|
||||
use alloc::boxed::Box;
|
||||
use persistent_store::{StorageError, StorageResult};
|
||||
|
||||
const PARTITION_LENGTH: usize = 0x41000;
|
||||
const METADATA_LENGTH: usize = 0x1000;
|
||||
|
||||
pub struct BufferUpgradeStorage {
|
||||
/// Content of the partition storage.
|
||||
partition: Box<[u8]>,
|
||||
}
|
||||
|
||||
impl BufferUpgradeStorage {
|
||||
pub fn new() -> StorageResult<BufferUpgradeStorage> {
|
||||
Ok(BufferUpgradeStorage {
|
||||
partition: vec![0xff; PARTITION_LENGTH].into_boxed_slice(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn read_partition(&self, offset: usize, length: usize) -> StorageResult<&[u8]> {
|
||||
if length == 0 {
|
||||
return Err(StorageError::OutOfBounds);
|
||||
}
|
||||
let partition_range = ModRange::new(0, self.partition.len());
|
||||
if partition_range.contains_range(&ModRange::new(offset, length)) {
|
||||
Ok(&self.partition[offset..][..length])
|
||||
} else {
|
||||
Err(StorageError::OutOfBounds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UpgradeStorage for BufferUpgradeStorage {
|
||||
fn write_bundle(&mut self, offset: usize, data: Vec<u8>) -> StorageResult<()> {
|
||||
if offset == 0 && data.len() != METADATA_LENGTH {
|
||||
return Err(StorageError::OutOfBounds);
|
||||
}
|
||||
if data.is_empty() {
|
||||
return Err(StorageError::OutOfBounds);
|
||||
}
|
||||
let partition_range = ModRange::new(0, self.partition.len());
|
||||
if partition_range.contains_range(&ModRange::new(offset, data.len())) {
|
||||
self.partition[offset..][..data.len()].copy_from_slice(&data);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(StorageError::OutOfBounds)
|
||||
}
|
||||
}
|
||||
|
||||
fn bundle_identifier(&self) -> u32 {
|
||||
0x60000
|
||||
}
|
||||
|
||||
fn running_firmware_version(&self) -> u64 {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn read_write_bundle() {
|
||||
let mut storage = BufferUpgradeStorage::new().unwrap();
|
||||
assert_eq!(storage.read_partition(0, 2).unwrap(), &[0xFF, 0xFF]);
|
||||
assert!(storage.write_bundle(1, vec![0x88, 0x88]).is_ok());
|
||||
assert_eq!(storage.read_partition(0, 2).unwrap(), &[0xFF, 0x88]);
|
||||
assert_eq!(
|
||||
storage.write_bundle(PARTITION_LENGTH - 1, vec![0x88, 0x88],),
|
||||
Err(StorageError::OutOfBounds)
|
||||
);
|
||||
assert_eq!(
|
||||
storage.read_partition(PARTITION_LENGTH - 2, 2).unwrap(),
|
||||
&[0xFF, 0xFF]
|
||||
);
|
||||
assert_eq!(
|
||||
storage.read_partition(PARTITION_LENGTH - 1, 2),
|
||||
Err(StorageError::OutOfBounds)
|
||||
);
|
||||
assert_eq!(
|
||||
storage.write_bundle(4, vec![]),
|
||||
Err(StorageError::OutOfBounds)
|
||||
);
|
||||
assert_eq!(
|
||||
storage.write_bundle(PARTITION_LENGTH + 4, vec![]),
|
||||
Err(StorageError::OutOfBounds)
|
||||
);
|
||||
assert_eq!(storage.read_partition(4, 0), Err(StorageError::OutOfBounds));
|
||||
assert_eq!(
|
||||
storage.read_partition(PARTITION_LENGTH + 4, 0),
|
||||
Err(StorageError::OutOfBounds)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partition_slice() {
|
||||
let storage = BufferUpgradeStorage::new().unwrap();
|
||||
assert_eq!(storage.bundle_identifier(), 0x60000);
|
||||
}
|
||||
}
|
||||
123
libraries/opensk/src/lib.rs
Normal file
123
libraries/opensk/src/lib.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
// Copyright 2019-2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
extern crate alloc;
|
||||
#[macro_use]
|
||||
extern crate arrayref;
|
||||
|
||||
use crate::ctap::hid::{HidPacket, HidPacketIterator};
|
||||
use crate::ctap::main_hid::MainHid;
|
||||
#[cfg(feature = "vendor_hid")]
|
||||
use crate::ctap::vendor_hid::VendorHid;
|
||||
use crate::ctap::CtapState;
|
||||
pub use crate::ctap::Transport;
|
||||
use crate::env::Env;
|
||||
|
||||
// Those macros should eventually be split into trace, debug, info, warn, and error macros when
|
||||
// adding either the defmt or log feature and crate dependency.
|
||||
#[cfg(feature = "debug_ctap")]
|
||||
macro_rules! debug_ctap {
|
||||
($env: expr, $($rest:tt)*) => {{
|
||||
use core::fmt::Write;
|
||||
writeln!($env.write(), $($rest)*).unwrap();
|
||||
}};
|
||||
}
|
||||
#[cfg(not(feature = "debug_ctap"))]
|
||||
macro_rules! debug_ctap {
|
||||
($env: expr, $($rest:tt)*) => {
|
||||
// To avoid unused variable warnings.
|
||||
let _ = $env;
|
||||
};
|
||||
}
|
||||
|
||||
pub mod api;
|
||||
// TODO(kaczmarczyck): Refactor this so that ctap module isn't public.
|
||||
pub mod ctap;
|
||||
pub mod env;
|
||||
#[cfg(feature = "std")]
|
||||
pub mod test_helpers;
|
||||
|
||||
/// CTAP implementation parameterized by its environment.
|
||||
pub struct Ctap<E: Env> {
|
||||
env: E,
|
||||
state: CtapState<E>,
|
||||
hid: MainHid<E>,
|
||||
#[cfg(feature = "vendor_hid")]
|
||||
vendor_hid: VendorHid<E>,
|
||||
}
|
||||
|
||||
impl<E: Env> Ctap<E> {
|
||||
/// Instantiates a CTAP implementation given its environment.
|
||||
// This should only take the environment, but it temporarily takes the boot time until the
|
||||
// clock is part of the environment.
|
||||
pub fn new(mut env: E) -> Self {
|
||||
let state = CtapState::<E>::new(&mut env);
|
||||
let hid = MainHid::default();
|
||||
#[cfg(feature = "vendor_hid")]
|
||||
let vendor_hid = VendorHid::default();
|
||||
Ctap {
|
||||
env,
|
||||
state,
|
||||
hid,
|
||||
#[cfg(feature = "vendor_hid")]
|
||||
vendor_hid,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn state(&mut self) -> &mut CtapState<E> {
|
||||
&mut self.state
|
||||
}
|
||||
|
||||
pub fn hid(&mut self) -> &mut MainHid<E> {
|
||||
&mut self.hid
|
||||
}
|
||||
|
||||
pub fn env(&mut self) -> &mut E {
|
||||
&mut self.env
|
||||
}
|
||||
|
||||
pub fn process_hid_packet(
|
||||
&mut self,
|
||||
packet: &HidPacket,
|
||||
transport: Transport,
|
||||
) -> HidPacketIterator {
|
||||
match transport {
|
||||
Transport::MainHid => {
|
||||
self.hid
|
||||
.process_hid_packet(&mut self.env, packet, &mut self.state)
|
||||
}
|
||||
#[cfg(feature = "vendor_hid")]
|
||||
Transport::VendorHid => {
|
||||
self.vendor_hid
|
||||
.process_hid_packet(&mut self.env, packet, &mut self.state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn should_wink(&mut self) -> bool {
|
||||
self.hid.should_wink(&mut self.env)
|
||||
}
|
||||
|
||||
#[cfg(feature = "with_ctap1")]
|
||||
pub fn u2f_grant_user_presence(&mut self) {
|
||||
self.state.u2f_grant_user_presence(&mut self.env)
|
||||
}
|
||||
|
||||
#[cfg(feature = "with_ctap1")]
|
||||
pub fn u2f_needs_user_presence(&mut self) -> bool {
|
||||
self.state.u2f_needs_user_presence(&mut self.env)
|
||||
}
|
||||
}
|
||||
61
libraries/opensk/src/test_helpers/mod.rs
Normal file
61
libraries/opensk/src/test_helpers/mod.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright 2022-2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::ctap::command::{
|
||||
AuthenticatorAttestationMaterial, AuthenticatorConfigParameters,
|
||||
AuthenticatorVendorConfigureParameters, Command, ATTESTATION_PRIVATE_KEY_LENGTH,
|
||||
};
|
||||
use crate::ctap::data_formats::ConfigSubCommand;
|
||||
use crate::ctap::status_code::Ctap2StatusCode;
|
||||
use crate::ctap::{Channel, CtapState};
|
||||
use crate::env::Env;
|
||||
|
||||
// In tests where we define a dummy user-presence check that immediately returns, the channel
|
||||
// ID is irrelevant, so we pass this (dummy but valid) value.
|
||||
const DUMMY_CHANNEL: Channel = Channel::MainHid([0x12, 0x34, 0x56, 0x78]);
|
||||
#[cfg(feature = "vendor_hid")]
|
||||
const VENDOR_CHANNEL: Channel = Channel::VendorHid([0x12, 0x34, 0x56, 0x78]);
|
||||
|
||||
pub fn enable_enterprise_attestation<E: Env>(
|
||||
state: &mut CtapState<E>,
|
||||
env: &mut E,
|
||||
) -> Result<AuthenticatorAttestationMaterial, Ctap2StatusCode> {
|
||||
let dummy_key = [0x41; ATTESTATION_PRIVATE_KEY_LENGTH];
|
||||
let dummy_cert = vec![0xdd; 20];
|
||||
let attestation_material = AuthenticatorAttestationMaterial {
|
||||
certificate: dummy_cert,
|
||||
private_key: dummy_key,
|
||||
};
|
||||
let configure_params = AuthenticatorVendorConfigureParameters {
|
||||
lockdown: false,
|
||||
attestation_material: Some(attestation_material.clone()),
|
||||
};
|
||||
#[cfg(feature = "vendor_hid")]
|
||||
let vendor_channel = VENDOR_CHANNEL;
|
||||
#[cfg(not(feature = "vendor_hid"))]
|
||||
let vendor_channel = DUMMY_CHANNEL;
|
||||
let vendor_command = Command::AuthenticatorVendorConfigure(configure_params);
|
||||
state.process_parsed_command(env, vendor_command, vendor_channel)?;
|
||||
|
||||
let config_params = AuthenticatorConfigParameters {
|
||||
sub_command: ConfigSubCommand::EnableEnterpriseAttestation,
|
||||
sub_command_params: None,
|
||||
pin_uv_auth_param: None,
|
||||
pin_uv_auth_protocol: None,
|
||||
};
|
||||
let config_command = Command::AuthenticatorConfig(config_params);
|
||||
state.process_parsed_command(env, config_command, DUMMY_CHANNEL)?;
|
||||
|
||||
Ok(attestation_material)
|
||||
}
|
||||
Reference in New Issue
Block a user