Skip to content

Commit a51bc42

Browse files
authored
test: Add signature verification for II attributes (dfinity#3611)
<!-- Make sure you talk to us before submitting changes. See CONTRIBUTING.md. --> # Motivation Stronger testing in Rust for the upcoming certified attributes feature. # Changes No production code changes (modulo derived traits). # Tests * Added signature verification test infrastructure for Internet Identity attributes with proper delegation and attribute signature validation
1 parent 8f39258 commit a51bc42

3 files changed

Lines changed: 180 additions & 29 deletions

File tree

src/canister_tests/src/framework.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use ic_representation_independent_hash::Value;
77
use identity_jose::jws::Decoder;
88
use internet_identity_interface::archive::types::*;
99
use internet_identity_interface::http_gateway::{HeaderField, HttpRequest};
10+
use internet_identity_interface::internet_identity::types::attributes::CertifiedAttribute;
1011
use internet_identity_interface::internet_identity::types::vc_mvp::SignedIdAlias;
1112
use internet_identity_interface::internet_identity::types::*;
1213
use lazy_static::lazy_static;
@@ -646,6 +647,40 @@ pub fn verify_delegation(
646647
.expect("delegation signature invalid");
647648
}
648649

650+
pub fn verify_attribute(
651+
env: &PocketIc,
652+
user_key: UserKey,
653+
certified_attribute: &CertifiedAttribute,
654+
expiration: u64,
655+
root_key: &[u8],
656+
) {
657+
const DOMAIN_SEPARATOR: &[u8] = b"ii-request-attribute";
658+
659+
// The signed message is a signature domain separator
660+
// followed by the representation independent hash of a map with entries
661+
// expiration, attribute-key/attribute-value.
662+
let key_value_pairs = vec![
663+
("expiration".to_string(), Value::Number(expiration)),
664+
(
665+
certified_attribute.key.clone(),
666+
Value::Bytes(certified_attribute.value.clone()),
667+
),
668+
];
669+
let mut msg: Vec<u8> = Vec::from([(DOMAIN_SEPARATOR.len() as u8)]);
670+
msg.extend_from_slice(DOMAIN_SEPARATOR);
671+
msg.extend_from_slice(
672+
&ic_representation_independent_hash::representation_independent_hash(&key_value_pairs),
673+
);
674+
675+
env.verify_canister_signature(
676+
msg,
677+
certified_attribute.signature.clone(),
678+
user_key.into_vec(),
679+
root_key.to_vec(),
680+
)
681+
.expect("attribute signature invalid");
682+
}
683+
649684
pub fn verify_id_alias_credential_via_env(
650685
env: &PocketIc,
651686
canister_sig_pk_der: CanisterSigPublicKeyDer,
Lines changed: 143 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,68 @@
11
//! Tests related to prepare_attributes and get_attributes II canister calls.
22
3+
use candid::Principal;
34
use canister_tests::api::internet_identity as api;
45
use canister_tests::framework::*;
6+
use ic_canister_sig_creation::extract_raw_canister_sig_pk_from_der;
57
use internet_identity_interface::internet_identity::types::attributes::{
68
CertifiedAttribute, CertifiedAttributes, GetAttributesRequest, PrepareAttributeRequest,
79
};
8-
use internet_identity_interface::internet_identity::types::OpenIdConfig;
10+
use internet_identity_interface::internet_identity::types::{
11+
GetDelegationResponse, OpenIdConfig, SignedDelegation,
12+
};
13+
use pocket_ic::PocketIc;
914
use pretty_assertions::assert_eq;
15+
use serde_bytes::ByteBuf;
1016
use std::time::Duration;
1117

18+
fn verify_delegation_and_attribute_signatures(
19+
env: &PocketIc,
20+
session_public_key: ByteBuf,
21+
user_key: ByteBuf,
22+
ii_backend_canister_id: Principal,
23+
signed_delegation: SignedDelegation,
24+
attributes: Vec<CertifiedAttribute>,
25+
attribute_expiration: u64,
26+
) {
27+
let root_key = env.root_key().unwrap();
28+
29+
let now_timestamp_ns = env.get_time().as_nanos_since_unix_epoch();
30+
31+
assert_eq!(signed_delegation.delegation.pubkey, session_public_key);
32+
assert!(
33+
signed_delegation.delegation.expiration > now_timestamp_ns,
34+
"Delegation has expired: {} <= {}",
35+
signed_delegation.delegation.expiration,
36+
now_timestamp_ns
37+
);
38+
39+
assert_eq!(signed_delegation.delegation.targets, None);
40+
assert!(attribute_expiration > now_timestamp_ns);
41+
42+
// Ensure that the user key is a canister signature (we rely on `user_key` being DER-encoded)
43+
let canister_id_bytes = extract_raw_canister_sig_pk_from_der(&user_key).unwrap();
44+
45+
let canister_id = {
46+
let canister_id_bytes_len = canister_id_bytes[0] as usize;
47+
let bound = canister_id_bytes_len + 1;
48+
Principal::from_slice(&canister_id_bytes[1..bound])
49+
};
50+
51+
assert_eq!(canister_id, ii_backend_canister_id);
52+
53+
verify_delegation(env, user_key.clone(), &signed_delegation, &root_key);
54+
55+
for attribute in &attributes {
56+
verify_attribute(
57+
env,
58+
user_key.clone(),
59+
attribute,
60+
attribute_expiration,
61+
&root_key,
62+
);
63+
}
64+
}
65+
1266
#[test]
1367
fn should_get_certified_attributes() {
1468
let env = env();
@@ -29,7 +83,7 @@ fn should_get_certified_attributes() {
2983
fedcm_uri: Some("https://accounts.google.com/gsi/fedcm.json".into()),
3084
}]);
3185

32-
let canister_id = install_ii_canister_with_arg_and_cycles(
86+
let ii_backend_canister_id = install_ii_canister_with_arg_and_cycles(
3387
&env,
3488
II_WASM.clone(),
3589
Some(init_args),
@@ -39,29 +93,40 @@ fn should_get_certified_attributes() {
3993
// This also handles the fetch triggered by initialize()
4094
crate::openid::mock_google_certs_response(&env);
4195

42-
deploy_archive_via_ii(&env, canister_id);
96+
deploy_archive_via_ii(&env, ii_backend_canister_id);
4397

4498
// Create an identity with the required authn method
45-
let user_number = crate::v2_api::authn_method_test_helpers::create_identity_with_authn_method(
46-
&env,
47-
canister_id,
48-
&test_authn_method,
49-
);
99+
let identity_number =
100+
crate::v2_api::authn_method_test_helpers::create_identity_with_authn_method(
101+
&env,
102+
ii_backend_canister_id,
103+
&test_authn_method,
104+
);
50105

51106
// Sync time to the JWT iat
52107
let time_to_advance = Duration::from_millis(test_time) - Duration::from_nanos(time(&env));
53108
env.advance_time(time_to_advance);
54109

55110
// Add OpenID credential
56-
api::openid_credential_add(&env, canister_id, test_principal, user_number, &jwt, &salt)
57-
.expect("failed to add openid credential")
58-
.expect("openid_credential_add error");
111+
api::openid_credential_add(
112+
&env,
113+
ii_backend_canister_id,
114+
test_principal,
115+
identity_number,
116+
&jwt,
117+
&salt,
118+
)
119+
.expect("failed to add openid credential")
120+
.expect("openid_credential_add error");
59121

60122
let origin = "https://some-dapp.com";
61123

62124
// 1. Prepare attributes
125+
126+
env.advance_time(Duration::from_secs(15));
127+
63128
let prepare_request = PrepareAttributeRequest {
64-
identity_number: user_number,
129+
identity_number,
65130
origin: origin.to_string(),
66131
account_number: None,
67132
attribute_keys: vec![
@@ -70,59 +135,110 @@ fn should_get_certified_attributes() {
70135
],
71136
};
72137

73-
let prepare_response =
74-
api::prepare_attributes(&env, canister_id, test_principal, prepare_request)
75-
.expect("failed to call prepare_attributes")
76-
.expect("prepare_attributes error");
138+
let prepare_response = api::prepare_attributes(
139+
&env,
140+
ii_backend_canister_id,
141+
test_principal,
142+
prepare_request,
143+
)
144+
.expect("failed to call prepare_attributes")
145+
.expect("prepare_attributes error");
77146

78147
assert_eq!(prepare_response.attributes.len(), 2);
79148

80149
// 2. Get attributes
150+
env.advance_time(Duration::from_secs(5));
151+
81152
let get_request = GetAttributesRequest {
82-
identity_number: user_number,
153+
identity_number,
83154
origin: origin.to_string(),
84155
account_number: None,
85156
issued_at_timestamp_ns: prepare_response.issued_at_timestamp_ns,
86157
attributes: prepare_response.attributes.clone(),
87158
};
88159

89-
let get_response = api::get_attributes(&env, canister_id, test_principal, get_request)
90-
.expect("failed to call get_attributes")
91-
.expect("get_attributes error");
160+
let get_response =
161+
api::get_attributes(&env, ii_backend_canister_id, test_principal, get_request)
162+
.expect("failed to call get_attributes")
163+
.expect("get_attributes error");
92164

93-
let mut actual_response = get_response;
94-
actual_response.certified_attributes.sort();
165+
let mut redacted_response = get_response.clone();
166+
redacted_response.certified_attributes.sort();
95167

96168
// Check the signatures (not a full verification step, just a smoke test)
97-
for attribute in actual_response.certified_attributes.iter() {
169+
for attribute in redacted_response.certified_attributes.iter() {
98170
assert!(attribute
99171
.signature
100172
.starts_with(&[217, 217, 247, 162, 107, 99, 101, 114]));
101173
}
102174

103175
// Redact the signatures so we can compare the response
104-
actual_response
176+
redacted_response
105177
.certified_attributes
106178
.iter_mut()
107179
.for_each(|attr| attr.signature = vec![]);
108180

109181
assert_eq!(
110-
actual_response,
182+
redacted_response,
111183
CertifiedAttributes {
112184
certified_attributes: vec![
113185
CertifiedAttribute {
114186
key: "openid:https://accounts.google.com:email".into(),
115187
value: b"andri.schatz@dfinity.org".to_vec(),
116-
signature: vec![] // redacted
188+
signature: vec![], // redacted
117189
},
118190
CertifiedAttribute {
119191
key: "openid:https://accounts.google.com:name".into(),
120192
value: b"Andri Schatz".to_vec(),
121-
signature: vec![],
193+
signature: vec![], // redacted
122194
},
123195
],
124196
expires_at_timestamp_ns: prepare_response.issued_at_timestamp_ns
125197
+ Duration::from_secs(30 * 60).as_nanos() as u64,
126198
}
127199
);
200+
201+
// Verify the signatures; this relies on delegation verification for the same (origin, user).
202+
203+
let session_public_key = ByteBuf::from("session public key");
204+
205+
env.advance_time(Duration::from_secs(35));
206+
207+
let (canister_sig_key, expiration) = api::prepare_delegation(
208+
&env,
209+
ii_backend_canister_id,
210+
test_principal,
211+
identity_number,
212+
origin,
213+
&session_public_key,
214+
None,
215+
)
216+
.unwrap();
217+
218+
env.advance_time(Duration::from_secs(5));
219+
220+
let signed_delegation = match api::get_delegation(
221+
&env,
222+
ii_backend_canister_id,
223+
test_principal,
224+
identity_number,
225+
origin,
226+
&session_public_key,
227+
expiration,
228+
)
229+
.unwrap()
230+
{
231+
GetDelegationResponse::SignedDelegation(delegation) => delegation,
232+
GetDelegationResponse::NoSuchDelegation => panic!("failed to get delegation"),
233+
};
234+
235+
verify_delegation_and_attribute_signatures(
236+
&env,
237+
session_public_key,
238+
canister_sig_key,
239+
ii_backend_canister_id,
240+
signed_delegation,
241+
get_response.certified_attributes,
242+
get_response.expires_at_timestamp_ns,
243+
);
128244
}

src/internet_identity_interface/src/internet_identity/types/attributes.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -402,14 +402,14 @@ impl TryFrom<GetAttributesRequest> for ValidatedGetAttributesRequest {
402402
}
403403
}
404404

405-
#[derive(Debug, PartialEq, CandidType, Serialize, Deserialize, Eq, PartialOrd, Ord)]
405+
#[derive(Clone, Debug, PartialEq, CandidType, Serialize, Deserialize, Eq, PartialOrd, Ord)]
406406
pub struct CertifiedAttribute {
407407
pub key: String,
408408
pub value: Vec<u8>,
409409
pub signature: Vec<u8>,
410410
}
411411

412-
#[derive(Debug, PartialEq, CandidType, Serialize, Deserialize)]
412+
#[derive(Clone, Debug, PartialEq, CandidType, Serialize, Deserialize)]
413413
pub struct CertifiedAttributes {
414414
pub certified_attributes: Vec<CertifiedAttribute>,
415415
pub expires_at_timestamp_ns: Timestamp,

0 commit comments

Comments
 (0)