-
Notifications
You must be signed in to change notification settings - Fork 3.7k
/
account.move
1604 lines (1392 loc) · 74.9 KB
/
account.move
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
module aptos_framework::account {
use std::bcs;
use std::error;
use std::hash;
use std::option::{Self, Option};
use std::signer;
use std::vector;
use aptos_framework::chain_id;
use aptos_framework::create_signer::create_signer;
use aptos_framework::event::{Self, EventHandle};
use aptos_framework::guid;
use aptos_framework::system_addresses;
use aptos_std::ed25519;
use aptos_std::from_bcs;
use aptos_std::multi_ed25519;
use aptos_std::table::{Self, Table};
use aptos_std::type_info::{Self, TypeInfo};
friend aptos_framework::aptos_account;
friend aptos_framework::coin;
friend aptos_framework::genesis;
friend aptos_framework::multisig_account;
friend aptos_framework::resource_account;
friend aptos_framework::transaction_validation;
#[event]
struct KeyRotation has drop, store {
account: address,
old_authentication_key: vector<u8>,
new_authentication_key: vector<u8>,
}
/// Resource representing an account.
struct Account has key, store {
authentication_key: vector<u8>,
sequence_number: u64,
guid_creation_num: u64,
coin_register_events: EventHandle<CoinRegisterEvent>,
key_rotation_events: EventHandle<KeyRotationEvent>,
rotation_capability_offer: CapabilityOffer<RotationCapability>,
signer_capability_offer: CapabilityOffer<SignerCapability>,
}
struct KeyRotationEvent has drop, store {
old_authentication_key: vector<u8>,
new_authentication_key: vector<u8>,
}
struct CoinRegisterEvent has drop, store {
type_info: TypeInfo,
}
#[event]
struct CoinRegister has drop, store {
account: address,
type_info: TypeInfo,
}
struct CapabilityOffer<phantom T> has store { for: Option<address> }
struct RotationCapability has drop, store { account: address }
struct SignerCapability has drop, store { account: address }
/// It is easy to fetch the authentication key of an address by simply reading it from the `Account` struct at that address.
/// The table in this struct makes it possible to do a reverse lookup: it maps an authentication key, to the address of the account which has that authentication key set.
///
/// This mapping is needed when recovering wallets for accounts whose authentication key has been rotated.
///
/// For example, imagine a freshly-created wallet with address `a` and thus also with authentication key `a`, derived from a PK `pk_a` with corresponding SK `sk_a`.
/// It is easy to recover such a wallet given just the secret key `sk_a`, since the PK can be derived from the SK, the authentication key can then be derived from the PK, and the address equals the authentication key (since there was no key rotation).
///
/// However, if such a wallet rotates its authentication key to `b` derived from a different PK `pk_b` with SK `sk_b`, how would account recovery work?
/// The recovered address would no longer be 'a'; it would be `b`, which is incorrect.
/// This struct solves this problem by mapping the new authentication key `b` to the original address `a` and thus helping the wallet software during recovery find the correct address.
struct OriginatingAddress has key {
address_map: Table<address, address>,
}
/// This structs stores the challenge message that should be signed during key rotation. First, this struct is
/// signed by the account owner's current public key, which proves possession of a capability to rotate the key.
/// Second, this struct is signed by the new public key that the account owner wants to rotate to, which proves
/// knowledge of this new public key's associated secret key. These two signatures cannot be replayed in another
/// context because they include the TXN's unique sequence number.
struct RotationProofChallenge has copy, drop {
sequence_number: u64,
// the sequence number of the account whose key is being rotated
originator: address,
// the address of the account whose key is being rotated
current_auth_key: address,
// the current authentication key of the account whose key is being rotated
new_public_key: vector<u8>,
// the new public key that the account owner wants to rotate to
}
/// Deprecated struct - newest version is `RotationCapabilityOfferProofChallengeV2`
struct RotationCapabilityOfferProofChallenge has drop {
sequence_number: u64,
recipient_address: address,
}
/// Deprecated struct - newest version is `SignerCapabilityOfferProofChallengeV2`
struct SignerCapabilityOfferProofChallenge has drop {
sequence_number: u64,
recipient_address: address,
}
/// This struct stores the challenge message that should be signed by the source account, when the source account
/// is delegating its rotation capability to the `recipient_address`.
/// This V2 struct adds the `chain_id` and `source_address` to the challenge message, which prevents replaying the challenge message.
struct RotationCapabilityOfferProofChallengeV2 has drop {
chain_id: u8,
sequence_number: u64,
source_address: address,
recipient_address: address,
}
struct SignerCapabilityOfferProofChallengeV2 has copy, drop {
sequence_number: u64,
source_address: address,
recipient_address: address,
}
const MAX_U64: u128 = 18446744073709551615;
const ZERO_AUTH_KEY: vector<u8> = x"0000000000000000000000000000000000000000000000000000000000000000";
/// Scheme identifier for Ed25519 signatures used to derive authentication keys for Ed25519 public keys.
const ED25519_SCHEME: u8 = 0;
/// Scheme identifier for MultiEd25519 signatures used to derive authentication keys for MultiEd25519 public keys.
const MULTI_ED25519_SCHEME: u8 = 1;
/// Scheme identifier used when hashing an account's address together with a seed to derive the address (not the
/// authentication key) of a resource account. This is an abuse of the notion of a scheme identifier which, for now,
/// serves to domain separate hashes used to derive resource account addresses from hashes used to derive
/// authentication keys. Without such separation, an adversary could create (and get a signer for) a resource account
/// whose address matches an existing address of a MultiEd25519 wallet.
const DERIVE_RESOURCE_ACCOUNT_SCHEME: u8 = 255;
/// Account already exists
const EACCOUNT_ALREADY_EXISTS: u64 = 1;
/// Account does not exist
const EACCOUNT_DOES_NOT_EXIST: u64 = 2;
/// Sequence number exceeds the maximum value for a u64
const ESEQUENCE_NUMBER_TOO_BIG: u64 = 3;
/// The provided authentication key has an invalid length
const EMALFORMED_AUTHENTICATION_KEY: u64 = 4;
/// Cannot create account because address is reserved
const ECANNOT_RESERVED_ADDRESS: u64 = 5;
/// Transaction exceeded its allocated max gas
const EOUT_OF_GAS: u64 = 6;
/// Specified current public key is not correct
const EWRONG_CURRENT_PUBLIC_KEY: u64 = 7;
/// Specified proof of knowledge required to prove ownership of a public key is invalid
const EINVALID_PROOF_OF_KNOWLEDGE: u64 = 8;
/// The caller does not have a digital-signature-based capability to call this function
const ENO_CAPABILITY: u64 = 9;
/// The caller does not have a valid rotation capability offer from the other account
const EINVALID_ACCEPT_ROTATION_CAPABILITY: u64 = 10;
/// Address to create is not a valid reserved address for Aptos framework
const ENO_VALID_FRAMEWORK_RESERVED_ADDRESS: u64 = 11;
/// Specified scheme required to proceed with the smart contract operation - can only be ED25519_SCHEME(0) OR MULTI_ED25519_SCHEME(1)
const EINVALID_SCHEME: u64 = 12;
/// Abort the transaction if the expected originating address is different from the originating address on-chain
const EINVALID_ORIGINATING_ADDRESS: u64 = 13;
/// The signer capability offer doesn't exist at the given address
const ENO_SUCH_SIGNER_CAPABILITY: u64 = 14;
/// An attempt to create a resource account on a claimed account
const ERESOURCE_ACCCOUNT_EXISTS: u64 = 15;
/// An attempt to create a resource account on an account that has a committed transaction
const EACCOUNT_ALREADY_USED: u64 = 16;
/// Offerer address doesn't exist
const EOFFERER_ADDRESS_DOES_NOT_EXIST: u64 = 17;
/// The specified rotation capability offer does not exist at the specified offerer address
const ENO_SUCH_ROTATION_CAPABILITY_OFFER: u64 = 18;
// The signer capability is not offered to any address
const ENO_SIGNER_CAPABILITY_OFFERED: u64 = 19;
// This account has exceeded the allocated GUIDs it can create. It should be impossible to reach this number for real applications.
const EEXCEEDED_MAX_GUID_CREATION_NUM: u64 = 20;
/// The new authentication key already has an entry in the `OriginatingAddress` table
const ENEW_AUTH_KEY_ALREADY_MAPPED: u64 = 21;
/// The current authentication key and the new authentication key are the same
const ENEW_AUTH_KEY_SAME_AS_CURRENT: u64 = 22;
/// Explicitly separate the GUID space between Object and Account to prevent accidental overlap.
const MAX_GUID_CREATION_NUM: u64 = 0x4000000000000;
#[test_only]
/// Create signer for testing, independently of an Aptos-style `Account`.
public fun create_signer_for_test(addr: address): signer { create_signer(addr) }
/// Only called during genesis to initialize system resources for this module.
public(friend) fun initialize(aptos_framework: &signer) {
system_addresses::assert_aptos_framework(aptos_framework);
move_to(aptos_framework, OriginatingAddress {
address_map: table::new(),
});
}
public fun create_account_if_does_not_exist(account_address: address) {
if (!exists<Account>(account_address)) {
create_account(account_address);
}
}
/// Publishes a new `Account` resource under `new_address`. A signer representing `new_address`
/// is returned. This way, the caller of this function can publish additional resources under
/// `new_address`.
public(friend) fun create_account(new_address: address): signer {
// there cannot be an Account resource under new_addr already.
assert!(!exists<Account>(new_address), error::already_exists(EACCOUNT_ALREADY_EXISTS));
// NOTE: @core_resources gets created via a `create_account` call, so we do not include it below.
assert!(
new_address != @vm_reserved && new_address != @aptos_framework && new_address != @aptos_token,
error::invalid_argument(ECANNOT_RESERVED_ADDRESS)
);
create_account_unchecked(new_address)
}
fun create_account_unchecked(new_address: address): signer {
let new_account = create_signer(new_address);
let authentication_key = bcs::to_bytes(&new_address);
assert!(
vector::length(&authentication_key) == 32,
error::invalid_argument(EMALFORMED_AUTHENTICATION_KEY)
);
let guid_creation_num = 0;
let guid_for_coin = guid::create(new_address, &mut guid_creation_num);
let coin_register_events = event::new_event_handle<CoinRegisterEvent>(guid_for_coin);
let guid_for_rotation = guid::create(new_address, &mut guid_creation_num);
let key_rotation_events = event::new_event_handle<KeyRotationEvent>(guid_for_rotation);
move_to(
&new_account,
Account {
authentication_key,
sequence_number: 0,
guid_creation_num,
coin_register_events,
key_rotation_events,
rotation_capability_offer: CapabilityOffer { for: option::none() },
signer_capability_offer: CapabilityOffer { for: option::none() },
}
);
new_account
}
#[view]
public fun exists_at(addr: address): bool {
exists<Account>(addr)
}
#[view]
public fun get_guid_next_creation_num(addr: address): u64 acquires Account {
borrow_global<Account>(addr).guid_creation_num
}
#[view]
public fun get_sequence_number(addr: address): u64 acquires Account {
borrow_global<Account>(addr).sequence_number
}
#[view]
public fun originating_address(auth_key: address): Option<address> acquires OriginatingAddress {
let address_map_ref = &borrow_global<OriginatingAddress>(@aptos_framework).address_map;
if (table::contains(address_map_ref, auth_key)) {
option::some(*table::borrow(address_map_ref, auth_key))
} else {
option::none()
}
}
public(friend) fun increment_sequence_number(addr: address) acquires Account {
let sequence_number = &mut borrow_global_mut<Account>(addr).sequence_number;
assert!(
(*sequence_number as u128) < MAX_U64,
error::out_of_range(ESEQUENCE_NUMBER_TOO_BIG)
);
*sequence_number = *sequence_number + 1;
}
#[view]
public fun get_authentication_key(addr: address): vector<u8> acquires Account {
borrow_global<Account>(addr).authentication_key
}
/// This function is used to rotate a resource account's authentication key to `new_auth_key`. This is done in
/// many contexts:
/// 1. During normal key rotation via `rotate_authentication_key` or `rotate_authentication_key_call`
/// 2. During resource account initialization so that no private key can control the resource account
/// 3. During multisig_v2 account creation
public(friend) fun rotate_authentication_key_internal(account: &signer, new_auth_key: vector<u8>) acquires Account {
let addr = signer::address_of(account);
assert!(exists_at(addr), error::not_found(EACCOUNT_DOES_NOT_EXIST));
assert!(
vector::length(&new_auth_key) == 32,
error::invalid_argument(EMALFORMED_AUTHENTICATION_KEY)
);
let account_resource = borrow_global_mut<Account>(addr);
account_resource.authentication_key = new_auth_key;
}
/// Private entry function for key rotation that allows the signer to update their authentication key.
/// Note that this does not update the `OriginatingAddress` table because the `new_auth_key` is not "verified": it
/// does not come with a proof-of-knowledge of the underlying SK. Nonetheless, we need this functionality due to
/// the introduction of non-standard key algorithms, such as passkeys, which cannot produce proofs-of-knowledge in
/// the format expected in `rotate_authentication_key`.
///
/// If you'd like to followup with updating the `OriginatingAddress` table, you can call
/// `set_originating_address()`.
entry fun rotate_authentication_key_call(account: &signer, new_auth_key: vector<u8>) acquires Account {
rotate_authentication_key_internal(account, new_auth_key);
}
/// Generic authentication key rotation function that allows the user to rotate their authentication key from any scheme to any scheme.
/// To authorize the rotation, we need two signatures:
/// - the first signature `cap_rotate_key` refers to the signature by the account owner's current key on a valid `RotationProofChallenge`,
/// demonstrating that the user intends to and has the capability to rotate the authentication key of this account;
/// - the second signature `cap_update_table` refers to the signature by the new key (that the account owner wants to rotate to) on a
/// valid `RotationProofChallenge`, demonstrating that the user owns the new private key, and has the authority to update the
/// `OriginatingAddress` map with the new address mapping `<new_address, originating_address>`.
/// To verify these two signatures, we need their corresponding public key and public key scheme: we use `from_scheme` and `from_public_key_bytes`
/// to verify `cap_rotate_key`, and `to_scheme` and `to_public_key_bytes` to verify `cap_update_table`.
/// A scheme of 0 refers to an Ed25519 key and a scheme of 1 refers to Multi-Ed25519 keys.
/// `originating address` refers to an account's original/first address.
///
/// Here is an example attack if we don't ask for the second signature `cap_update_table`:
/// Alice has rotated her account `addr_a` to `new_addr_a`. As a result, the following entry is created, to help Alice when recovering her wallet:
/// `OriginatingAddress[new_addr_a]` -> `addr_a`
/// Alice has had a bad day: her laptop blew up and she needs to reset her account on a new one.
/// (Fortunately, she still has her secret key `new_sk_a` associated with her new address `new_addr_a`, so she can do this.)
///
/// But Bob likes to mess with Alice.
/// Bob creates an account `addr_b` and maliciously rotates it to Alice's new address `new_addr_a`. Since we are no longer checking a PoK,
/// Bob can easily do this.
///
/// Now, the table will be updated to make Alice's new address point to Bob's address: `OriginatingAddress[new_addr_a]` -> `addr_b`.
/// When Alice recovers her account, her wallet will display the attacker's address (Bob's) `addr_b` as her address.
/// Now Alice will give `addr_b` to everyone to pay her, but the money will go to Bob.
///
/// Because we ask for a valid `cap_update_table`, this kind of attack is not possible. Bob would not have the secret key of Alice's address
/// to rotate his address to Alice's address in the first place.
public entry fun rotate_authentication_key(
account: &signer,
from_scheme: u8,
from_public_key_bytes: vector<u8>,
to_scheme: u8,
to_public_key_bytes: vector<u8>,
cap_rotate_key: vector<u8>,
cap_update_table: vector<u8>,
) acquires Account, OriginatingAddress {
let addr = signer::address_of(account);
assert!(exists_at(addr), error::not_found(EACCOUNT_DOES_NOT_EXIST));
let account_resource = borrow_global_mut<Account>(addr);
// Verify the given `from_public_key_bytes` matches this account's current authentication key.
if (from_scheme == ED25519_SCHEME) {
let from_pk = ed25519::new_unvalidated_public_key_from_bytes(from_public_key_bytes);
let from_auth_key = ed25519::unvalidated_public_key_to_authentication_key(&from_pk);
assert!(
account_resource.authentication_key == from_auth_key,
error::unauthenticated(EWRONG_CURRENT_PUBLIC_KEY)
);
} else if (from_scheme == MULTI_ED25519_SCHEME) {
let from_pk = multi_ed25519::new_unvalidated_public_key_from_bytes(from_public_key_bytes);
let from_auth_key = multi_ed25519::unvalidated_public_key_to_authentication_key(&from_pk);
assert!(
account_resource.authentication_key == from_auth_key,
error::unauthenticated(EWRONG_CURRENT_PUBLIC_KEY)
);
} else {
abort error::invalid_argument(EINVALID_SCHEME)
};
// Construct a valid `RotationProofChallenge` that `cap_rotate_key` and `cap_update_table` will validate against.
let curr_auth_key_as_address = from_bcs::to_address(account_resource.authentication_key);
let challenge = RotationProofChallenge {
sequence_number: account_resource.sequence_number,
originator: addr,
current_auth_key: curr_auth_key_as_address,
new_public_key: to_public_key_bytes,
};
// Assert the challenges signed by the current and new keys are valid
assert_valid_rotation_proof_signature_and_get_auth_key(
from_scheme,
from_public_key_bytes,
cap_rotate_key,
&challenge
);
let new_auth_key = assert_valid_rotation_proof_signature_and_get_auth_key(
to_scheme,
to_public_key_bytes,
cap_update_table,
&challenge
);
// Update the `OriginatingAddress` table.
update_auth_key_and_originating_address_table(addr, account_resource, new_auth_key);
}
public entry fun rotate_authentication_key_with_rotation_capability(
delegate_signer: &signer,
rotation_cap_offerer_address: address,
new_scheme: u8,
new_public_key_bytes: vector<u8>,
cap_update_table: vector<u8>
) acquires Account, OriginatingAddress {
assert!(exists_at(rotation_cap_offerer_address), error::not_found(EOFFERER_ADDRESS_DOES_NOT_EXIST));
// Check that there exists a rotation capability offer at the offerer's account resource for the delegate.
let delegate_address = signer::address_of(delegate_signer);
let offerer_account_resource = borrow_global<Account>(rotation_cap_offerer_address);
assert!(
option::contains(&offerer_account_resource.rotation_capability_offer.for, &delegate_address),
error::not_found(ENO_SUCH_ROTATION_CAPABILITY_OFFER)
);
let curr_auth_key = from_bcs::to_address(offerer_account_resource.authentication_key);
let challenge = RotationProofChallenge {
sequence_number: get_sequence_number(delegate_address),
originator: rotation_cap_offerer_address,
current_auth_key: curr_auth_key,
new_public_key: new_public_key_bytes,
};
// Verifies that the `RotationProofChallenge` from above is signed under the new public key that we are rotating to. l
let new_auth_key = assert_valid_rotation_proof_signature_and_get_auth_key(
new_scheme,
new_public_key_bytes,
cap_update_table,
&challenge
);
// Update the `OriginatingAddress` table, so we can find the originating address using the new address.
let offerer_account_resource = borrow_global_mut<Account>(rotation_cap_offerer_address);
update_auth_key_and_originating_address_table(
rotation_cap_offerer_address,
offerer_account_resource,
new_auth_key
);
}
/// Offers rotation capability on behalf of `account` to the account at address `recipient_address`.
/// An account can delegate its rotation capability to only one other address at one time. If the account
/// has an existing rotation capability offer, calling this function will update the rotation capability offer with
/// the new `recipient_address`.
/// Here, `rotation_capability_sig_bytes` signature indicates that this key rotation is authorized by the account owner,
/// and prevents the classic "time-of-check time-of-use" attack.
/// For example, users usually rely on what the wallet displays to them as the transaction's outcome. Consider a contract that with 50% probability
/// (based on the current timestamp in Move), rotates somebody's key. The wallet might be unlucky and get an outcome where nothing is rotated,
/// incorrectly telling the user nothing bad will happen. But when the transaction actually gets executed, the attacker gets lucky and
/// the execution path triggers the account key rotation.
/// We prevent such attacks by asking for this extra signature authorizing the key rotation.
///
/// @param rotation_capability_sig_bytes is the signature by the account owner's key on `RotationCapabilityOfferProofChallengeV2`.
/// @param account_scheme is the scheme of the account (ed25519 or multi_ed25519).
/// @param account_public_key_bytes is the public key of the account owner.
/// @param recipient_address is the address of the recipient of the rotation capability - note that if there's an existing rotation capability
/// offer, calling this function will replace the previous `recipient_address` upon successful verification.
public entry fun offer_rotation_capability(
account: &signer,
rotation_capability_sig_bytes: vector<u8>,
account_scheme: u8,
account_public_key_bytes: vector<u8>,
recipient_address: address,
) acquires Account {
let addr = signer::address_of(account);
assert!(exists_at(recipient_address), error::not_found(EACCOUNT_DOES_NOT_EXIST));
// proof that this account intends to delegate its rotation capability to another account
let account_resource = borrow_global_mut<Account>(addr);
let proof_challenge = RotationCapabilityOfferProofChallengeV2 {
chain_id: chain_id::get(),
sequence_number: account_resource.sequence_number,
source_address: addr,
recipient_address,
};
// verify the signature on `RotationCapabilityOfferProofChallengeV2` by the account owner
if (account_scheme == ED25519_SCHEME) {
let pubkey = ed25519::new_unvalidated_public_key_from_bytes(account_public_key_bytes);
let expected_auth_key = ed25519::unvalidated_public_key_to_authentication_key(&pubkey);
assert!(
account_resource.authentication_key == expected_auth_key,
error::invalid_argument(EWRONG_CURRENT_PUBLIC_KEY)
);
let rotation_capability_sig = ed25519::new_signature_from_bytes(rotation_capability_sig_bytes);
assert!(
ed25519::signature_verify_strict_t(&rotation_capability_sig, &pubkey, proof_challenge),
error::invalid_argument(EINVALID_PROOF_OF_KNOWLEDGE)
);
} else if (account_scheme == MULTI_ED25519_SCHEME) {
let pubkey = multi_ed25519::new_unvalidated_public_key_from_bytes(account_public_key_bytes);
let expected_auth_key = multi_ed25519::unvalidated_public_key_to_authentication_key(&pubkey);
assert!(
account_resource.authentication_key == expected_auth_key,
error::invalid_argument(EWRONG_CURRENT_PUBLIC_KEY)
);
let rotation_capability_sig = multi_ed25519::new_signature_from_bytes(rotation_capability_sig_bytes);
assert!(
multi_ed25519::signature_verify_strict_t(&rotation_capability_sig, &pubkey, proof_challenge),
error::invalid_argument(EINVALID_PROOF_OF_KNOWLEDGE)
);
} else {
abort error::invalid_argument(EINVALID_SCHEME)
};
// update the existing rotation capability offer or put in a new rotation capability offer for the current account
option::swap_or_fill(&mut account_resource.rotation_capability_offer.for, recipient_address);
}
/// For the given account, add an entry to `OriginatingAddress` table mapping the account's
/// authentication key to the account's address.
///
/// Can be used as a followup to `rotate_authentication_key_call()` to reconcile the
/// `OriginatingAddress` table, or to establish a mapping for a new account that has not yet had
/// its authentication key rotated.
///
/// Aborts if there is already an entry in the `OriginatingAddress` table for the account's
/// authentication key.
///
/// Kept as a private entry function to ensure that after an unproven rotation via
/// `rotate_authentication_key_call()`, the `OriginatingAddress` table is only updated under the
/// authority of the new authentication key.
entry fun set_originating_address(account: &signer) acquires Account, OriginatingAddress {
let account_addr = signer::address_of(account);
assert!(exists<Account>(account_addr), error::not_found(EACCOUNT_DOES_NOT_EXIST));
let auth_key_as_address =
from_bcs::to_address(borrow_global<Account>(account_addr).authentication_key);
let address_map_ref_mut =
&mut borrow_global_mut<OriginatingAddress>(@aptos_framework).address_map;
if (table::contains(address_map_ref_mut, auth_key_as_address)) {
assert!(
*table::borrow(address_map_ref_mut, auth_key_as_address) == account_addr,
error::invalid_argument(ENEW_AUTH_KEY_ALREADY_MAPPED)
);
} else {
table::add(address_map_ref_mut, auth_key_as_address, account_addr);
};
}
#[view]
/// Returns true if the account at `account_addr` has a rotation capability offer.
public fun is_rotation_capability_offered(account_addr: address): bool acquires Account {
let account_resource = borrow_global<Account>(account_addr);
option::is_some(&account_resource.rotation_capability_offer.for)
}
#[view]
/// Returns the address of the account that has a rotation capability offer from the account at `account_addr`.
public fun get_rotation_capability_offer_for(account_addr: address): address acquires Account {
let account_resource = borrow_global<Account>(account_addr);
assert!(
option::is_some(&account_resource.rotation_capability_offer.for),
error::not_found(ENO_SIGNER_CAPABILITY_OFFERED),
);
*option::borrow(&account_resource.rotation_capability_offer.for)
}
/// Revoke the rotation capability offer given to `to_be_revoked_recipient_address` from `account`
public entry fun revoke_rotation_capability(account: &signer, to_be_revoked_address: address) acquires Account {
assert!(exists_at(to_be_revoked_address), error::not_found(EACCOUNT_DOES_NOT_EXIST));
let addr = signer::address_of(account);
let account_resource = borrow_global<Account>(addr);
assert!(
option::contains(&account_resource.rotation_capability_offer.for, &to_be_revoked_address),
error::not_found(ENO_SUCH_ROTATION_CAPABILITY_OFFER)
);
revoke_any_rotation_capability(account);
}
/// Revoke any rotation capability offer in the specified account.
public entry fun revoke_any_rotation_capability(account: &signer) acquires Account {
let account_resource = borrow_global_mut<Account>(signer::address_of(account));
option::extract(&mut account_resource.rotation_capability_offer.for);
}
/// Offers signer capability on behalf of `account` to the account at address `recipient_address`.
/// An account can delegate its signer capability to only one other address at one time.
/// `signer_capability_key_bytes` is the `SignerCapabilityOfferProofChallengeV2` signed by the account owner's key
/// `account_scheme` is the scheme of the account (ed25519 or multi_ed25519).
/// `account_public_key_bytes` is the public key of the account owner.
/// `recipient_address` is the address of the recipient of the signer capability - note that if there's an existing
/// `recipient_address` in the account owner's `SignerCapabilityOffer`, this will replace the
/// previous `recipient_address` upon successful verification (the previous recipient will no longer have access
/// to the account owner's signer capability).
public entry fun offer_signer_capability(
account: &signer,
signer_capability_sig_bytes: vector<u8>,
account_scheme: u8,
account_public_key_bytes: vector<u8>,
recipient_address: address
) acquires Account {
let source_address = signer::address_of(account);
assert!(exists_at(recipient_address), error::not_found(EACCOUNT_DOES_NOT_EXIST));
// Proof that this account intends to delegate its signer capability to another account.
let proof_challenge = SignerCapabilityOfferProofChallengeV2 {
sequence_number: get_sequence_number(source_address),
source_address,
recipient_address,
};
verify_signed_message(
source_address, account_scheme, account_public_key_bytes, signer_capability_sig_bytes, proof_challenge);
// Update the existing signer capability offer or put in a new signer capability offer for the recipient.
let account_resource = borrow_global_mut<Account>(source_address);
option::swap_or_fill(&mut account_resource.signer_capability_offer.for, recipient_address);
}
#[view]
/// Returns true if the account at `account_addr` has a signer capability offer.
public fun is_signer_capability_offered(account_addr: address): bool acquires Account {
let account_resource = borrow_global<Account>(account_addr);
option::is_some(&account_resource.signer_capability_offer.for)
}
#[view]
/// Returns the address of the account that has a signer capability offer from the account at `account_addr`.
public fun get_signer_capability_offer_for(account_addr: address): address acquires Account {
let account_resource = borrow_global<Account>(account_addr);
assert!(
option::is_some(&account_resource.signer_capability_offer.for),
error::not_found(ENO_SIGNER_CAPABILITY_OFFERED),
);
*option::borrow(&account_resource.signer_capability_offer.for)
}
/// Revoke the account owner's signer capability offer for `to_be_revoked_address` (i.e., the address that
/// has a signer capability offer from `account` but will be revoked in this function).
public entry fun revoke_signer_capability(account: &signer, to_be_revoked_address: address) acquires Account {
assert!(exists_at(to_be_revoked_address), error::not_found(EACCOUNT_DOES_NOT_EXIST));
let addr = signer::address_of(account);
let account_resource = borrow_global<Account>(addr);
assert!(
option::contains(&account_resource.signer_capability_offer.for, &to_be_revoked_address),
error::not_found(ENO_SUCH_SIGNER_CAPABILITY)
);
revoke_any_signer_capability(account);
}
/// Revoke any signer capability offer in the specified account.
public entry fun revoke_any_signer_capability(account: &signer) acquires Account {
let account_resource = borrow_global_mut<Account>(signer::address_of(account));
option::extract(&mut account_resource.signer_capability_offer.for);
}
/// Return an authorized signer of the offerer, if there's an existing signer capability offer for `account`
/// at the offerer's address.
public fun create_authorized_signer(account: &signer, offerer_address: address): signer acquires Account {
assert!(exists_at(offerer_address), error::not_found(EOFFERER_ADDRESS_DOES_NOT_EXIST));
// Check if there's an existing signer capability offer from the offerer.
let account_resource = borrow_global<Account>(offerer_address);
let addr = signer::address_of(account);
assert!(
option::contains(&account_resource.signer_capability_offer.for, &addr),
error::not_found(ENO_SUCH_SIGNER_CAPABILITY)
);
create_signer(offerer_address)
}
///////////////////////////////////////////////////////////////////////////
/// Helper functions for authentication key rotation.
///////////////////////////////////////////////////////////////////////////
fun assert_valid_rotation_proof_signature_and_get_auth_key(
scheme: u8,
public_key_bytes: vector<u8>,
signature: vector<u8>,
challenge: &RotationProofChallenge
): vector<u8> {
if (scheme == ED25519_SCHEME) {
let pk = ed25519::new_unvalidated_public_key_from_bytes(public_key_bytes);
let sig = ed25519::new_signature_from_bytes(signature);
assert!(
ed25519::signature_verify_strict_t(&sig, &pk, *challenge),
std::error::invalid_argument(EINVALID_PROOF_OF_KNOWLEDGE)
);
ed25519::unvalidated_public_key_to_authentication_key(&pk)
} else if (scheme == MULTI_ED25519_SCHEME) {
let pk = multi_ed25519::new_unvalidated_public_key_from_bytes(public_key_bytes);
let sig = multi_ed25519::new_signature_from_bytes(signature);
assert!(
multi_ed25519::signature_verify_strict_t(&sig, &pk, *challenge),
std::error::invalid_argument(EINVALID_PROOF_OF_KNOWLEDGE)
);
multi_ed25519::unvalidated_public_key_to_authentication_key(&pk)
} else {
abort error::invalid_argument(EINVALID_SCHEME)
}
}
/// Update the `OriginatingAddress` table, so that we can find the originating address using the latest address
/// in the event of key recovery.
fun update_auth_key_and_originating_address_table(
originating_addr: address,
account_resource: &mut Account,
new_auth_key_vector: vector<u8>,
) acquires OriginatingAddress {
let address_map = &mut borrow_global_mut<OriginatingAddress>(@aptos_framework).address_map;
let curr_auth_key = from_bcs::to_address(account_resource.authentication_key);
let new_auth_key = from_bcs::to_address(new_auth_key_vector);
assert!(
new_auth_key != curr_auth_key,
error::invalid_argument(ENEW_AUTH_KEY_SAME_AS_CURRENT)
);
// Checks `OriginatingAddress[curr_auth_key]` is either unmapped, or mapped to `originating_address`.
// If it's mapped to the originating address, removes that mapping.
// Otherwise, abort if it's mapped to a different address.
if (table::contains(address_map, curr_auth_key)) {
// If account_a with address_a is rotating its keypair from keypair_a to keypair_b, we expect
// the address of the account to stay the same, while its keypair updates to keypair_b.
// Here, by asserting that we're calling from the account with the originating address, we enforce
// the standard of keeping the same address and updating the keypair at the contract level.
// Without this assertion, the dapps could also update the account's address to address_b (the address that
// is programmatically related to keypaier_b) and update the keypair to keypair_b. This causes problems
// for interoperability because different dapps can implement this in different ways.
// If the account with address b calls this function with two valid signatures, it will abort at this step,
// because address b is not the account's originating address.
assert!(
originating_addr == table::remove(address_map, curr_auth_key),
error::not_found(EINVALID_ORIGINATING_ADDRESS)
);
};
// Set `OriginatingAddress[new_auth_key] = originating_address`.
assert!(
!table::contains(address_map, new_auth_key),
error::invalid_argument(ENEW_AUTH_KEY_ALREADY_MAPPED)
);
table::add(address_map, new_auth_key, originating_addr);
if (std::features::module_event_migration_enabled()) {
event::emit(KeyRotation {
account: originating_addr,
old_authentication_key: account_resource.authentication_key,
new_authentication_key: new_auth_key_vector,
});
} else {
event::emit_event<KeyRotationEvent>(
&mut account_resource.key_rotation_events,
KeyRotationEvent {
old_authentication_key: account_resource.authentication_key,
new_authentication_key: new_auth_key_vector,
}
);
};
// Update the account resource's authentication key.
account_resource.authentication_key = new_auth_key_vector;
}
///////////////////////////////////////////////////////////////////////////
/// Basic account creation methods.
///////////////////////////////////////////////////////////////////////////
/// This is a helper function to compute resource addresses. Computation of the address
/// involves the use of a cryptographic hash operation and should be use thoughtfully.
public fun create_resource_address(source: &address, seed: vector<u8>): address {
let bytes = bcs::to_bytes(source);
vector::append(&mut bytes, seed);
vector::push_back(&mut bytes, DERIVE_RESOURCE_ACCOUNT_SCHEME);
from_bcs::to_address(hash::sha3_256(bytes))
}
/// A resource account is used to manage resources independent of an account managed by a user.
/// In Aptos a resource account is created based upon the sha3 256 of the source's address and additional seed data.
/// A resource account can only be created once, this is designated by setting the
/// `Account::signer_capability_offer::for` to the address of the resource account. While an entity may call
/// `create_account` to attempt to claim an account ahead of the creation of a resource account, if found Aptos will
/// transition ownership of the account over to the resource account. This is done by validating that the account has
/// yet to execute any transactions and that the `Account::signer_capability_offer::for` is none. The probability of a
/// collision where someone has legitimately produced a private key that maps to a resource account address is less
/// than `(1/2)^(256)`.
public fun create_resource_account(source: &signer, seed: vector<u8>): (signer, SignerCapability) acquires Account {
let resource_addr = create_resource_address(&signer::address_of(source), seed);
let resource = if (exists_at(resource_addr)) {
let account = borrow_global<Account>(resource_addr);
assert!(
option::is_none(&account.signer_capability_offer.for),
error::already_exists(ERESOURCE_ACCCOUNT_EXISTS),
);
assert!(
account.sequence_number == 0,
error::invalid_state(EACCOUNT_ALREADY_USED),
);
create_signer(resource_addr)
} else {
create_account_unchecked(resource_addr)
};
// By default, only the SignerCapability should have control over the resource account and not the auth key.
// If the source account wants direct control via auth key, they would need to explicitly rotate the auth key
// of the resource account using the SignerCapability.
rotate_authentication_key_internal(&resource, ZERO_AUTH_KEY);
let account = borrow_global_mut<Account>(resource_addr);
account.signer_capability_offer.for = option::some(resource_addr);
let signer_cap = SignerCapability { account: resource_addr };
(resource, signer_cap)
}
/// create the account for system reserved addresses
public(friend) fun create_framework_reserved_account(addr: address): (signer, SignerCapability) {
assert!(
addr == @0x1 ||
addr == @0x2 ||
addr == @0x3 ||
addr == @0x4 ||
addr == @0x5 ||
addr == @0x6 ||
addr == @0x7 ||
addr == @0x8 ||
addr == @0x9 ||
addr == @0xa,
error::permission_denied(ENO_VALID_FRAMEWORK_RESERVED_ADDRESS),
);
let signer = create_account_unchecked(addr);
let signer_cap = SignerCapability { account: addr };
(signer, signer_cap)
}
///////////////////////////////////////////////////////////////////////////
/// GUID management methods.
///////////////////////////////////////////////////////////////////////////
public fun create_guid(account_signer: &signer): guid::GUID acquires Account {
let addr = signer::address_of(account_signer);
let account = borrow_global_mut<Account>(addr);
let guid = guid::create(addr, &mut account.guid_creation_num);
assert!(
account.guid_creation_num < MAX_GUID_CREATION_NUM,
error::out_of_range(EEXCEEDED_MAX_GUID_CREATION_NUM),
);
guid
}
///////////////////////////////////////////////////////////////////////////
/// GUID management methods.
///////////////////////////////////////////////////////////////////////////
public fun new_event_handle<T: drop + store>(account: &signer): EventHandle<T> acquires Account {
event::new_event_handle(create_guid(account))
}
///////////////////////////////////////////////////////////////////////////
/// Coin management methods.
///////////////////////////////////////////////////////////////////////////
public(friend) fun register_coin<CoinType>(account_addr: address) acquires Account {
let account = borrow_global_mut<Account>(account_addr);
if (std::features::module_event_migration_enabled()) {
event::emit(
CoinRegister {
account: account_addr,
type_info: type_info::type_of<CoinType>(),
},
);
} else {
event::emit_event<CoinRegisterEvent>(
&mut account.coin_register_events,
CoinRegisterEvent {
type_info: type_info::type_of<CoinType>(),
},
);
}
}
///////////////////////////////////////////////////////////////////////////
// Test-only create signerCapabilityOfferProofChallengeV2 and return it
///////////////////////////////////////////////////////////////////////////
#[test_only]
public fun get_signer_capability_offer_proof_challenge_v2(
source_address: address,
recipient_address: address,
): SignerCapabilityOfferProofChallengeV2 acquires Account {
SignerCapabilityOfferProofChallengeV2 {
sequence_number: borrow_global_mut<Account>(source_address).sequence_number,
source_address,
recipient_address,
}
}
///////////////////////////////////////////////////////////////////////////
/// Capability based functions for efficient use.
///////////////////////////////////////////////////////////////////////////
public fun create_signer_with_capability(capability: &SignerCapability): signer {
let addr = &capability.account;
create_signer(*addr)
}
public fun get_signer_capability_address(capability: &SignerCapability): address {
capability.account
}
public fun verify_signed_message<T: drop>(
account: address,
account_scheme: u8,
account_public_key: vector<u8>,
signed_message_bytes: vector<u8>,
message: T,
) acquires Account {
let account_resource = borrow_global<Account>(account);
// Verify that the `SignerCapabilityOfferProofChallengeV2` has the right information and is signed by the account owner's key
if (account_scheme == ED25519_SCHEME) {
let pubkey = ed25519::new_unvalidated_public_key_from_bytes(account_public_key);
let expected_auth_key = ed25519::unvalidated_public_key_to_authentication_key(&pubkey);
assert!(
account_resource.authentication_key == expected_auth_key,
error::invalid_argument(EWRONG_CURRENT_PUBLIC_KEY),
);
let signer_capability_sig = ed25519::new_signature_from_bytes(signed_message_bytes);
assert!(
ed25519::signature_verify_strict_t(&signer_capability_sig, &pubkey, message),
error::invalid_argument(EINVALID_PROOF_OF_KNOWLEDGE),
);
} else if (account_scheme == MULTI_ED25519_SCHEME) {
let pubkey = multi_ed25519::new_unvalidated_public_key_from_bytes(account_public_key);
let expected_auth_key = multi_ed25519::unvalidated_public_key_to_authentication_key(&pubkey);
assert!(
account_resource.authentication_key == expected_auth_key,
error::invalid_argument(EWRONG_CURRENT_PUBLIC_KEY),
);
let signer_capability_sig = multi_ed25519::new_signature_from_bytes(signed_message_bytes);
assert!(
multi_ed25519::signature_verify_strict_t(&signer_capability_sig, &pubkey, message),
error::invalid_argument(EINVALID_PROOF_OF_KNOWLEDGE),
);
} else {
abort error::invalid_argument(EINVALID_SCHEME)
};
}
#[test_only]
public fun create_account_for_test(new_address: address): signer {
// Make this easier by just allowing the account to be created again in a test
if (!exists_at(new_address)) {
create_account_unchecked(new_address)
} else {
create_signer_for_test(new_address)
}
}
#[test]
/// Assert correct signer creation.
fun test_create_signer_for_test() {
assert!(signer::address_of(&create_signer_for_test(@aptos_framework)) == @0x1, 0);
assert!(signer::address_of(&create_signer_for_test(@0x123)) == @0x123, 0);
}
#[test(user = @0x1)]
public entry fun test_create_resource_account(user: signer) acquires Account {
let (resource_account, resource_account_cap) = create_resource_account(&user, x"01");
let resource_addr = signer::address_of(&resource_account);
assert!(resource_addr != signer::address_of(&user), 0);
assert!(resource_addr == get_signer_capability_address(&resource_account_cap), 1);
}
#[test]
#[expected_failure(abort_code = 0x10007, location = Self)]
public entry fun test_cannot_control_resource_account_via_auth_key() acquires Account {
let alice_pk = x"4141414141414141414141414141414141414141414141414141414141414145";
let alice = create_account_from_ed25519_public_key(alice_pk);
let alice_auth = get_authentication_key(signer::address_of(&alice)); // must look like a valid public key
let (eve_sk, eve_pk) = ed25519::generate_keys();
let eve_pk_bytes = ed25519::validated_public_key_to_bytes(&eve_pk);
let eve = create_account_from_ed25519_public_key(eve_pk_bytes);
let recipient_address = signer::address_of(&eve);
let seed = eve_pk_bytes; // multisig public key
vector::push_back(&mut seed, 1); // multisig threshold
vector::push_back(&mut seed, 1); // signature scheme id
let (resource, _) = create_resource_account(&alice, seed);
let resource_addr = signer::address_of(&resource);
let proof_challenge = SignerCapabilityOfferProofChallengeV2 {
sequence_number: borrow_global_mut<Account>(resource_addr).sequence_number,
source_address: resource_addr,
recipient_address,
};
let eve_sig = ed25519::sign_struct(&eve_sk, copy proof_challenge);
// Construct a malicious 1-out-of-2 multisig PK over Alice's authentication key and Eve's Ed25519 PK.