Skip to main content

ant_core/
update.rs

1use std::path::{Path, PathBuf};
2
3use ant_protocol::pqc::api::{ml_dsa_65, MlDsaPublicKey, MlDsaSignature, MlDsaVariant};
4use futures_util::StreamExt;
5use serde::{Deserialize, Serialize};
6
7use crate::error::{Error, Result};
8use crate::node::binary::{extract_tar_gz, extract_zip, ProgressReporter};
9
10const GITHUB_REPO: &str = "WithAutonomi/ant-client";
11const CLI_BINARY_NAME: &str = "ant";
12const TAG_PREFIX: &str = "ant-cli-v";
13
14/// Signing context for domain separation (must match the context used by `ant-keygen sign`).
15const SIGNING_CONTEXT: &[u8] = b"ant-release-v1";
16
17/// ML-DSA-65 signature size in bytes.
18const SIGNATURE_SIZE: usize = 3309;
19
20/// Embedded release signing public key (ML-DSA-65).
21///
22/// This key is used to verify signatures on released ant-client binaries.
23/// The corresponding private key is held by authorised release signers.
24/// Generated: 2026-03-30 19:49:36 UTC
25const RELEASE_SIGNING_KEY: &[u8] = &[
26    0xb4, 0xcf, 0x2a, 0x24, 0x31, 0xd9, 0xb2, 0x3a, 0xab, 0xe9, 0x5e, 0xfc, 0xbc, 0xf3, 0xb1, 0x1f,
27    0x4e, 0x50, 0x0a, 0x46, 0xeb, 0x83, 0xfc, 0x6f, 0x0f, 0x89, 0x41, 0x00, 0x1b, 0x52, 0xde, 0xdc,
28    0xb5, 0xc9, 0x07, 0xed, 0x72, 0x3a, 0xe1, 0xa9, 0x82, 0xe1, 0xfc, 0xff, 0xab, 0xce, 0x9f, 0x7d,
29    0xab, 0xe2, 0x57, 0x92, 0xe0, 0xf2, 0xac, 0xa1, 0x41, 0xd3, 0x74, 0x95, 0x41, 0xd1, 0xac, 0x22,
30    0xb1, 0xbb, 0x5c, 0xf4, 0x02, 0x0b, 0x73, 0x85, 0xfd, 0x56, 0x75, 0x0d, 0x5c, 0x38, 0xe1, 0x2d,
31    0xe0, 0x15, 0x4f, 0xbf, 0x40, 0xeb, 0xf1, 0x0d, 0x8f, 0x39, 0x32, 0xeb, 0x80, 0xa7, 0x2e, 0x77,
32    0x3e, 0x54, 0xe3, 0x3d, 0x62, 0xae, 0xe7, 0x09, 0x5e, 0xfb, 0xdc, 0xaa, 0x07, 0xdc, 0xe1, 0x08,
33    0x96, 0xbb, 0x4b, 0xa0, 0x2e, 0x05, 0x2d, 0xee, 0xeb, 0x9a, 0x1a, 0xae, 0xde, 0xe9, 0x2c, 0xf8,
34    0x2f, 0x43, 0x6c, 0x78, 0x4b, 0xde, 0xef, 0x91, 0x8b, 0x94, 0x9d, 0x4f, 0x81, 0x05, 0xcc, 0xf0,
35    0x98, 0xce, 0xce, 0x67, 0x54, 0xac, 0xde, 0xcd, 0x26, 0x9e, 0x84, 0xf4, 0x88, 0xb2, 0x1a, 0x3e,
36    0x93, 0x2e, 0xff, 0xa8, 0x45, 0x95, 0xd1, 0xd0, 0xb1, 0x6c, 0x3c, 0x1e, 0xef, 0x3d, 0xe3, 0xf2,
37    0x73, 0xe2, 0xf6, 0xb7, 0xf9, 0x3f, 0x10, 0x0d, 0x3c, 0xde, 0x28, 0x94, 0x07, 0xef, 0x24, 0x70,
38    0xc4, 0x5a, 0x0a, 0x67, 0xbb, 0x0f, 0x4f, 0x5c, 0x2b, 0xd8, 0x02, 0x05, 0xa5, 0x98, 0x03, 0x5d,
39    0x8f, 0xc0, 0x4a, 0x84, 0xe9, 0xea, 0xac, 0x13, 0xdf, 0x69, 0xfc, 0x1e, 0xcf, 0xb6, 0x88, 0xba,
40    0x99, 0x30, 0xbc, 0x7a, 0xb8, 0x9d, 0x3d, 0x62, 0x3b, 0x33, 0x19, 0xbb, 0x3a, 0x2c, 0x2b, 0xa0,
41    0x5a, 0xb0, 0x8f, 0x9e, 0x10, 0x81, 0xb9, 0x12, 0x54, 0x81, 0xf8, 0xe2, 0x91, 0xb2, 0xe7, 0xe6,
42    0x9c, 0x11, 0xeb, 0x49, 0x64, 0x3a, 0x25, 0xd6, 0x53, 0x2e, 0xdf, 0xfc, 0x14, 0x32, 0x65, 0xcc,
43    0x87, 0xdb, 0xfd, 0xbb, 0x81, 0xa8, 0x50, 0xcc, 0xb4, 0x31, 0x0c, 0x70, 0xf3, 0xb6, 0x15, 0x8a,
44    0x50, 0x80, 0xad, 0xb1, 0xb0, 0x10, 0x2b, 0x67, 0x33, 0xf5, 0xf6, 0x36, 0x35, 0x5f, 0xa7, 0xd2,
45    0x81, 0xd6, 0x75, 0xa1, 0x18, 0x15, 0xbe, 0x1d, 0x5e, 0x33, 0x8e, 0x98, 0xdd, 0x45, 0x0f, 0x0c,
46    0x0f, 0x0d, 0x8b, 0x3f, 0x97, 0x11, 0x21, 0x2e, 0xa0, 0x5e, 0xfe, 0x70, 0x09, 0xa7, 0x14, 0x30,
47    0xa3, 0x01, 0x2d, 0x18, 0x2b, 0x8f, 0x19, 0x75, 0x54, 0x1f, 0xd8, 0xee, 0x66, 0x06, 0x7b, 0x9d,
48    0x7d, 0xb2, 0xae, 0x14, 0xe6, 0x51, 0x19, 0xc2, 0x45, 0x2e, 0x7e, 0x11, 0xd9, 0x7b, 0x16, 0x8e,
49    0xae, 0x17, 0xdb, 0x1b, 0x24, 0x90, 0xcd, 0xed, 0x94, 0xf9, 0xf7, 0xba, 0x9f, 0x4c, 0x12, 0xae,
50    0x31, 0x7b, 0xd4, 0x7c, 0x04, 0x42, 0x2c, 0x32, 0x16, 0xc1, 0x70, 0x6d, 0x11, 0x6f, 0x3b, 0x44,
51    0x62, 0xba, 0xbd, 0xc5, 0x7a, 0xec, 0x55, 0x1b, 0xcd, 0xdb, 0xb6, 0x55, 0x08, 0x86, 0x13, 0x7f,
52    0x4e, 0xa9, 0x63, 0xe1, 0x87, 0xa0, 0x7e, 0x49, 0xfb, 0xf4, 0xa3, 0x46, 0xcf, 0x1d, 0xec, 0xf5,
53    0xc6, 0x2f, 0xe1, 0x43, 0x02, 0xd0, 0xe4, 0x5f, 0x1b, 0x20, 0x1a, 0xa7, 0x81, 0xbd, 0x31, 0x19,
54    0x6a, 0x74, 0xd7, 0x9a, 0x6d, 0x3d, 0xf8, 0xac, 0x4d, 0xbb, 0x01, 0x63, 0xa4, 0x9d, 0x3c, 0xc9,
55    0x6c, 0x8a, 0x4f, 0x61, 0xd6, 0x98, 0xf5, 0x40, 0x22, 0xa9, 0x5e, 0x93, 0x5e, 0x13, 0xd3, 0xe0,
56    0xdb, 0x54, 0xab, 0x0d, 0xe3, 0x88, 0x85, 0x80, 0x7a, 0x5e, 0x38, 0x64, 0x97, 0xc4, 0xe9, 0xb0,
57    0x5d, 0xf7, 0x40, 0x5f, 0x6e, 0x3f, 0xbe, 0x14, 0x9a, 0x7c, 0xa2, 0x7c, 0x74, 0xa5, 0x32, 0x22,
58    0x61, 0x31, 0xa5, 0x0d, 0xa5, 0xcc, 0x93, 0xe4, 0xfd, 0xed, 0xbc, 0xe7, 0xf2, 0xe5, 0xdb, 0xc6,
59    0x0c, 0xe1, 0xc8, 0x4e, 0xee, 0xe6, 0x76, 0x1c, 0x10, 0x1b, 0xd8, 0x53, 0xd4, 0xe8, 0x07, 0xed,
60    0xea, 0x91, 0xd4, 0x1b, 0x91, 0x5c, 0x28, 0x05, 0xca, 0xe2, 0x9c, 0xd1, 0x99, 0x43, 0xed, 0xd8,
61    0x6a, 0x2b, 0xd2, 0x64, 0x9b, 0xe1, 0x0c, 0x88, 0x6c, 0x0d, 0xb2, 0x6b, 0x73, 0x85, 0x9d, 0xbf,
62    0x79, 0x78, 0xaa, 0x7b, 0x5e, 0xf8, 0xa4, 0x26, 0xdf, 0xb3, 0x9b, 0x24, 0x5a, 0xc8, 0x19, 0x22,
63    0xa5, 0xc6, 0xca, 0x00, 0x59, 0x3b, 0xad, 0x45, 0xdf, 0x71, 0x1d, 0x60, 0x60, 0x24, 0x0d, 0xa4,
64    0x3d, 0x42, 0x23, 0xb8, 0xfe, 0xac, 0x86, 0x94, 0x79, 0x87, 0x05, 0xae, 0xb8, 0x4d, 0x7e, 0x11,
65    0x5b, 0x22, 0x44, 0x15, 0x3d, 0x7f, 0x82, 0x98, 0x65, 0x0a, 0x3c, 0xd3, 0xef, 0x80, 0x0d, 0x75,
66    0x03, 0x92, 0xf6, 0x3a, 0x8b, 0xa4, 0xb0, 0x61, 0x2d, 0x2c, 0xcc, 0x1f, 0x01, 0x8e, 0x7a, 0x46,
67    0x36, 0x2a, 0x83, 0x21, 0x88, 0x98, 0x13, 0x0a, 0xd5, 0xa1, 0x54, 0x4b, 0x63, 0xe0, 0xe3, 0x1c,
68    0x07, 0x5e, 0x32, 0x8c, 0xa4, 0x6b, 0x62, 0xc3, 0x28, 0x95, 0xb8, 0x0a, 0xb9, 0x4f, 0xaf, 0x7f,
69    0x49, 0xeb, 0xff, 0xd7, 0xa1, 0x41, 0x43, 0x9a, 0x92, 0x9a, 0x5f, 0xee, 0xbd, 0xb9, 0xbe, 0xb3,
70    0x4b, 0x9d, 0x0b, 0xcb, 0x9b, 0x2c, 0x26, 0x8f, 0x0f, 0xb1, 0xfa, 0xc0, 0xe3, 0x3a, 0x7f, 0x2b,
71    0x51, 0x79, 0x75, 0x25, 0x5b, 0x23, 0x22, 0xb1, 0x01, 0x27, 0x4e, 0x43, 0xdd, 0x66, 0x7a, 0x33,
72    0x4d, 0x32, 0x96, 0x83, 0x59, 0x52, 0xd7, 0x3c, 0xb0, 0xe3, 0x03, 0xd6, 0xb0, 0xc7, 0x99, 0x68,
73    0xc6, 0xa4, 0x2d, 0x35, 0x3d, 0xa4, 0x6a, 0x17, 0xd9, 0xf4, 0x0c, 0x26, 0x11, 0xe4, 0xbc, 0x03,
74    0x87, 0x25, 0x62, 0xad, 0xa9, 0x7e, 0x96, 0x4d, 0x39, 0x9b, 0x8f, 0x09, 0xdc, 0xd1, 0x28, 0x5e,
75    0xf4, 0xe3, 0x94, 0xfd, 0x94, 0x46, 0x30, 0xe2, 0x24, 0x46, 0x30, 0x7f, 0xf4, 0x4c, 0xaa, 0x51,
76    0x7e, 0x04, 0x5c, 0xa4, 0x8c, 0xba, 0x4a, 0xb8, 0x61, 0x5e, 0x75, 0x1c, 0xa8, 0x0c, 0xbc, 0x7f,
77    0x36, 0x16, 0xa1, 0x72, 0x98, 0x6a, 0x44, 0x39, 0x42, 0x67, 0xb5, 0x4a, 0xac, 0x14, 0x35, 0x8f,
78    0xcd, 0x87, 0x3f, 0x9e, 0x2e, 0xa1, 0x53, 0xf1, 0x45, 0x68, 0x26, 0xcb, 0x35, 0x96, 0x57, 0xd5,
79    0x3a, 0x24, 0x74, 0xe2, 0xff, 0xe0, 0x70, 0xb1, 0xbd, 0xec, 0x0c, 0xd2, 0x97, 0x9a, 0xe5, 0x9f,
80    0xa9, 0xfe, 0x6a, 0x63, 0x17, 0x35, 0xad, 0x64, 0x2f, 0xd9, 0x2e, 0xdb, 0x47, 0xdc, 0x62, 0xdc,
81    0xcc, 0xee, 0x7e, 0x23, 0xa6, 0x67, 0x61, 0x7c, 0xd1, 0x03, 0xbd, 0x78, 0xe9, 0x34, 0x05, 0xed,
82    0x05, 0x87, 0xef, 0x59, 0xf4, 0x16, 0xd6, 0x8d, 0x85, 0x46, 0x65, 0x2a, 0x08, 0xac, 0x4a, 0x5d,
83    0xe6, 0x27, 0x5f, 0x43, 0xdd, 0x51, 0x4e, 0x95, 0x9b, 0xf5, 0x0c, 0x81, 0x24, 0x73, 0x39, 0x77,
84    0xe9, 0xc8, 0x35, 0x4a, 0xe2, 0xb8, 0x35, 0x92, 0xde, 0x5c, 0x31, 0x12, 0x36, 0x5c, 0xc7, 0x69,
85    0xcd, 0x79, 0xa9, 0xf9, 0xcf, 0x13, 0xa9, 0x12, 0x29, 0x25, 0x5c, 0x6a, 0x34, 0xa4, 0xbf, 0xc5,
86    0xb6, 0x2a, 0xc1, 0xba, 0x6a, 0xd3, 0x98, 0x8c, 0x9b, 0x6d, 0x9f, 0xb9, 0x25, 0xa6, 0xd1, 0x97,
87    0x80, 0x38, 0x11, 0xdc, 0x73, 0x5c, 0xe7, 0x3a, 0x1f, 0xd2, 0x16, 0xcd, 0x63, 0xfb, 0x41, 0xb0,
88    0xba, 0xb0, 0x38, 0x67, 0x48, 0xd2, 0x8a, 0x94, 0x2f, 0x11, 0x81, 0xbf, 0x66, 0x38, 0x68, 0xff,
89    0xfe, 0xd1, 0x7c, 0xcd, 0xa3, 0xac, 0xe4, 0xf7, 0x58, 0x19, 0xcd, 0x2a, 0xe3, 0xfa, 0x4d, 0xb0,
90    0xbe, 0xac, 0x05, 0x1c, 0xd9, 0x8d, 0xf7, 0x5c, 0xc0, 0xfc, 0xa6, 0xb5, 0x99, 0xb8, 0x8e, 0x2b,
91    0x72, 0xf8, 0x19, 0xfc, 0x17, 0x11, 0xf6, 0x2b, 0x08, 0xe4, 0x6e, 0xb0, 0x65, 0xab, 0x78, 0x8a,
92    0xfc, 0x7c, 0x09, 0xca, 0x73, 0xcd, 0x35, 0x5d, 0x6c, 0x7a, 0x36, 0xc0, 0x24, 0xba, 0x3f, 0x08,
93    0xea, 0x17, 0x09, 0xe1, 0x9d, 0x5d, 0x18, 0x59, 0x8a, 0xd8, 0x6a, 0x6d, 0x85, 0x6a, 0x9e, 0xa9,
94    0xe5, 0x4b, 0x45, 0xb2, 0x35, 0x6e, 0x62, 0x24, 0x08, 0x00, 0x1c, 0x06, 0x73, 0x27, 0x5d, 0x11,
95    0x4a, 0xc8, 0x51, 0xbd, 0x59, 0xd6, 0x94, 0xce, 0x16, 0x15, 0x17, 0x58, 0x7f, 0x39, 0x9d, 0x4e,
96    0x69, 0x1a, 0x64, 0xbb, 0xd4, 0x51, 0xb9, 0xe4, 0x7d, 0x51, 0x3a, 0xff, 0xe5, 0x1f, 0x29, 0xea,
97    0x7e, 0xa5, 0x62, 0x63, 0xff, 0x10, 0xf7, 0x54, 0x35, 0xd1, 0xf3, 0x73, 0x1e, 0xab, 0xca, 0x52,
98    0x14, 0xc6, 0x7e, 0x51, 0xc2, 0x48, 0x13, 0xcb, 0x30, 0xb2, 0x1a, 0x84, 0x72, 0xe5, 0x44, 0x83,
99    0xc9, 0x90, 0xa5, 0x8c, 0xf9, 0xeb, 0x3c, 0x5c, 0xc6, 0xcc, 0x8a, 0x95, 0x8a, 0xfa, 0xeb, 0x37,
100    0x9c, 0xde, 0xa2, 0xb1, 0x72, 0x4d, 0xd9, 0x3d, 0xab, 0xfd, 0x0e, 0xbd, 0x32, 0x9d, 0x23, 0xe9,
101    0x6f, 0x85, 0x4e, 0xfe, 0xcd, 0x91, 0xfb, 0x82, 0x94, 0xee, 0x8b, 0xdf, 0x6a, 0xd9, 0x01, 0xa1,
102    0xc6, 0x22, 0x18, 0x01, 0x8d, 0x10, 0xd5, 0x87, 0x42, 0xd0, 0xbd, 0x23, 0x75, 0x44, 0x53, 0x46,
103    0xa5, 0xae, 0x00, 0x4c, 0x0e, 0x88, 0x4a, 0xa8, 0x3d, 0x4a, 0x30, 0xe0, 0x1a, 0xa4, 0xe5, 0x40,
104    0xb8, 0xe0, 0x12, 0x9c, 0x44, 0x03, 0xfb, 0x2e, 0x4e, 0xf5, 0x29, 0xdb, 0x09, 0x84, 0x55, 0xc7,
105    0x6c, 0xc6, 0x1f, 0xf9, 0xee, 0x0b, 0xa4, 0x91, 0x7d, 0x79, 0x27, 0x59, 0x75, 0x97, 0xec, 0x6a,
106    0xa8, 0xf8, 0x55, 0xa8, 0x45, 0xd4, 0xd7, 0xa6, 0xc1, 0xc4, 0x27, 0x35, 0xe8, 0x4f, 0x39, 0x89,
107    0x7d, 0x41, 0xf3, 0xf6, 0xd0, 0xb6, 0xf9, 0x91, 0xeb, 0x94, 0xf1, 0xbb, 0x17, 0x46, 0x9c, 0xd5,
108    0x5a, 0x53, 0x04, 0x2d, 0x12, 0x7c, 0x17, 0x6a, 0x36, 0xb5, 0xea, 0xf3, 0x5b, 0x96, 0x1b, 0xee,
109    0xce, 0xc4, 0xc0, 0x11, 0x5a, 0xbc, 0x0c, 0x29, 0xd0, 0x42, 0x1d, 0x16, 0x63, 0xea, 0x1e, 0x04,
110    0x2f, 0xe3, 0x17, 0xed, 0x33, 0xac, 0x56, 0x80, 0x34, 0x41, 0x41, 0x1e, 0x77, 0x80, 0x06, 0x9f,
111    0xbc, 0x2e, 0x78, 0xa1, 0x04, 0x00, 0x06, 0x6f, 0x36, 0x2f, 0xb7, 0xa5, 0x95, 0x37, 0x82, 0x9d,
112    0xef, 0x41, 0x08, 0x85, 0x3d, 0x53, 0xa7, 0xfb, 0xfe, 0xba, 0x8c, 0xb9, 0xae, 0xc7, 0x89, 0x11,
113    0x69, 0x4f, 0x62, 0xe6, 0xb6, 0x08, 0x6b, 0x35, 0x1c, 0x96, 0xb3, 0x7b, 0x40, 0x2d, 0xee, 0x07,
114    0x40, 0x52, 0x4f, 0x68, 0x60, 0xf4, 0xb9, 0xc3, 0x54, 0x9f, 0x22, 0x50, 0x88, 0x48, 0x6a, 0x28,
115    0x93, 0x46, 0x00, 0xe2, 0x4a, 0x85, 0x41, 0x78, 0x0e, 0x87, 0xc5, 0xeb, 0xfc, 0xd3, 0x5f, 0x4d,
116    0x24, 0xe4, 0x9d, 0xeb, 0x1d, 0x00, 0x73, 0x85, 0x25, 0x47, 0x9e, 0x8c, 0x5b, 0x88, 0xf4, 0x3b,
117    0x33, 0xf0, 0x3d, 0x3a, 0xa1, 0x28, 0xd3, 0x06, 0xb4, 0x7a, 0x4e, 0x5d, 0x31, 0x1b, 0xca, 0xf4,
118    0x3f, 0x70, 0x30, 0x49, 0x44, 0x29, 0x24, 0x14, 0x5e, 0x35, 0xc2, 0x6c, 0x92, 0x7e, 0xf8, 0x97,
119    0x0c, 0x51, 0x9d, 0x67, 0xc0, 0x10, 0xa9, 0x35, 0x48, 0x59, 0x6a, 0x33, 0xef, 0x40, 0x4e, 0x53,
120    0x10, 0x14, 0x2a, 0x12, 0x38, 0xe6, 0xc4, 0x63, 0x9c, 0x84, 0x85, 0x06, 0xaf, 0x3d, 0x3a, 0x84,
121    0x06, 0x60, 0x88, 0x32, 0xda, 0x2c, 0xe5, 0xc6, 0x59, 0xf1, 0xe0, 0x10, 0xe2, 0x3c, 0xe6, 0xbf,
122    0x32, 0x7d, 0x32, 0x39, 0x6d, 0xe4, 0xd9, 0xca, 0xe7, 0xf5, 0xf4, 0xa6, 0x5f, 0xb2, 0x33, 0x05,
123    0xb5, 0xad, 0x5f, 0xcb, 0x0b, 0x14, 0xaf, 0xeb, 0xc0, 0xec, 0x87, 0x85, 0x9b, 0x13, 0xb5, 0x8a,
124    0x98, 0xa9, 0x92, 0x13, 0x1b, 0x74, 0xec, 0xfd, 0xe1, 0xc1, 0x22, 0x06, 0x5d, 0x4f, 0x06, 0xc7,
125    0xdd, 0xc6, 0xf0, 0xc4, 0x01, 0x04, 0xad, 0x7f, 0x71, 0xbc, 0x74, 0x4d, 0xfd, 0x18, 0xa3, 0x56,
126    0x2c, 0x45, 0x28, 0x2b, 0x2f, 0xbc, 0x9b, 0xb8, 0x4b, 0xe6, 0x51, 0x75, 0x28, 0x0c, 0x27, 0x0e,
127    0xf7, 0x92, 0x8c, 0xc9, 0xde, 0x33, 0x1a, 0x65, 0x28, 0xc7, 0x01, 0x32, 0xa2, 0x36, 0x88, 0xb6,
128    0x64, 0x10, 0x03, 0xd6, 0xb7, 0x9f, 0x9d, 0x73, 0xe1, 0xa9, 0xc7, 0xdf, 0xe1, 0x0b, 0x39, 0x31,
129    0x77, 0xbc, 0x91, 0xf1, 0x45, 0x9a, 0xc5, 0x97, 0x28, 0xc0, 0x61, 0xc5, 0x23, 0x54, 0xad, 0xe3,
130    0x23, 0x18, 0x69, 0xf7, 0x27, 0xd0, 0x5b, 0xf2, 0x44, 0x62, 0xdc, 0x97, 0xce, 0x4e, 0x40, 0x76,
131    0x00, 0xde, 0xc2, 0xf9, 0x3a, 0x42, 0xfd, 0xd4, 0xd7, 0xe1, 0x85, 0xd8, 0xc9, 0x38, 0x91, 0xc1,
132    0x79, 0x87, 0x58, 0xf1, 0x26, 0x1a, 0x29, 0x02, 0xe3, 0x54, 0xde, 0x58, 0x64, 0x9d, 0xe6, 0x8e,
133    0x33, 0x70, 0x53, 0x43, 0x47, 0x90, 0xee, 0x6e, 0x0f, 0x8c, 0xb3, 0x9e, 0x47, 0x45, 0xfc, 0xa8,
134    0xe3, 0x52, 0x62, 0x74, 0x6d, 0xa2, 0xaf, 0x28, 0x9d, 0xdf, 0x1e, 0x69, 0x1f, 0x56, 0xbc, 0x49,
135    0xc1, 0xe5, 0xd6, 0xc4, 0xb5, 0x5c, 0x4d, 0x39, 0x49, 0x4b, 0xb4, 0xec, 0x56, 0x54, 0x9a, 0x15,
136    0x94, 0x0a, 0xcb, 0xa9, 0x10, 0x46, 0x03, 0x5c, 0x23, 0x2f, 0x29, 0xed, 0x72, 0xa1, 0x57, 0xfa,
137    0x58, 0xef, 0x21, 0x7e, 0xf2, 0x8b, 0xa7, 0x04, 0x51, 0xb4, 0x03, 0x5d, 0xd8, 0x48, 0xc0, 0xe5,
138    0x83, 0xb6, 0x7a, 0x6b, 0xcd, 0xfb, 0xda, 0x47, 0xe8, 0xa1, 0xae, 0x57, 0x74, 0x49, 0xc0, 0xf9,
139    0x4a, 0x6b, 0x3c, 0xb5, 0xd8, 0x27, 0x3d, 0x1d, 0x96, 0x39, 0x09, 0x65, 0x95, 0xdb, 0x01, 0xa3,
140    0x8b, 0x78, 0x8b, 0x07, 0x6d, 0x1c, 0x8b, 0x4b, 0x1d, 0x9d, 0x4a, 0x4f, 0xcb, 0xb8, 0xf6, 0x22,
141    0x73, 0x8a, 0x7b, 0xc8, 0xf2, 0x0a, 0xef, 0x03, 0x1e, 0xb7, 0x4d, 0x8f, 0xc0, 0xdf, 0x87, 0x88,
142    0x05, 0xe1, 0x0a, 0x30, 0xea, 0xde, 0xf3, 0xc2, 0xb6, 0x00, 0x3c, 0xd6, 0xff, 0x3b, 0xb5, 0x01,
143    0xfb, 0xd8, 0xb2, 0x65, 0x26, 0x5d, 0xa0, 0x5a, 0x7c, 0xef, 0x1d, 0x85, 0xbe, 0x51, 0xc2, 0x57,
144    0x0b, 0x27, 0x37, 0x71, 0x99, 0xf5, 0x87, 0x83, 0x68, 0x0b, 0x88, 0xed, 0x66, 0x9e, 0x37, 0x59,
145    0x84, 0x23, 0x72, 0xc3, 0x80, 0xac, 0xfe, 0x45, 0x5f, 0xdf, 0x31, 0xc4, 0x84, 0x07, 0x5a, 0x17,
146    0x28, 0xcd, 0x64, 0xb4, 0xe2, 0xa3, 0x0e, 0x2c, 0x15, 0x60, 0x77, 0xdc, 0x08, 0x45, 0x36, 0x37,
147    0x68, 0x50, 0xba, 0x03, 0x85, 0xb7, 0xed, 0xd0, 0x7b, 0xb2, 0xa1, 0x62, 0xbc, 0x70, 0x00, 0x9e,
148];
149
150/// Result of checking whether an update is available.
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct UpdateCheck {
153    pub current_version: String,
154    pub latest_version: String,
155    pub update_available: bool,
156    pub download_url: Option<String>,
157}
158
159impl UpdateCheck {
160    /// Force the check to report an update even if already on the latest version.
161    ///
162    /// Populates `download_url` using the current `latest_version`.
163    pub fn force(&mut self) -> Result<()> {
164        self.update_available = true;
165        self.download_url = Some(build_download_url(&self.latest_version)?);
166        Ok(())
167    }
168}
169
170/// Result of a completed update.
171#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct UpdateResult {
173    pub previous_version: String,
174    pub new_version: String,
175}
176
177/// Check whether a newer version is available on GitHub Releases.
178///
179/// Compares `current_version` against the latest release tag using semantic versioning.
180pub async fn check_for_update(current_version: &str) -> Result<UpdateCheck> {
181    let latest = fetch_latest_cli_version().await?;
182    let current = parse_version(current_version)?;
183    let latest_parsed = parse_version(&latest)?;
184    let update_available = latest_parsed > current;
185
186    let download_url = if update_available {
187        Some(build_download_url(&latest)?)
188    } else {
189        None
190    };
191
192    Ok(UpdateCheck {
193        current_version: current_version.to_string(),
194        latest_version: latest,
195        update_available,
196        download_url,
197    })
198}
199
200/// Download and install the update, replacing the current binary.
201///
202/// Downloads the release archive and its detached `.sig` file, verifies the
203/// ML-DSA-65 signature against the embedded release key, extracts the binary,
204/// and replaces the running executable.
205pub async fn perform_update(
206    check: &UpdateCheck,
207    progress: &dyn ProgressReporter,
208) -> Result<UpdateResult> {
209    let download_url = check.download_url.as_deref().ok_or_else(|| {
210        Error::UpdateFailed("no download URL — are you already on the latest version?".to_string())
211    })?;
212
213    let tmp_dir = tempfile::tempdir()
214        .map_err(|e| Error::UpdateFailed(format!("failed to create temp directory: {e}")))?;
215
216    let (archive_path, archive_bytes) =
217        download_archive(download_url, tmp_dir.path(), progress).await?;
218
219    let sig_url = format!("{download_url}.sig");
220    progress.report_started("Downloading signature...");
221    let sig_bytes = download_bytes(&sig_url).await?;
222
223    progress.report_started("Verifying ML-DSA signature...");
224    verify_signature(&archive_bytes, &sig_bytes)?;
225    progress.report_complete("Signature verified");
226
227    progress.report_started("Extracting archive...");
228    let extracted = if download_url.ends_with(".zip") {
229        extract_zip(&archive_bytes, tmp_dir.path(), CLI_BINARY_NAME)?
230    } else {
231        extract_tar_gz(&archive_bytes, tmp_dir.path(), CLI_BINARY_NAME)?
232    };
233    let binary_path = extracted.binary_path;
234
235    // Verify the extracted binary reports the expected version.
236    let actual_version = extract_version(&binary_path).await;
237    if let Ok(ref v) = actual_version {
238        if v != &check.latest_version {
239            return Err(Error::UpdateFailed(format!(
240                "version mismatch: expected {}, binary reports {v}",
241                check.latest_version
242            )));
243        }
244    }
245
246    replace_binary(&binary_path)?;
247
248    // Clean up is handled by tmp_dir Drop, but remove the archive explicitly
249    // since it can be large.
250    let _ = std::fs::remove_file(&archive_path);
251
252    Ok(UpdateResult {
253        previous_version: check.current_version.clone(),
254        new_version: check.latest_version.clone(),
255    })
256}
257
258/// Fetch the latest stable CLI release version from GitHub.
259///
260/// Lists all releases and finds the newest non-draft, non-prerelease one whose
261/// tag starts with `ant-cli-v`.
262async fn fetch_latest_cli_version() -> Result<String> {
263    // per_page=100 is the GitHub API maximum; covers repos with many release tags.
264    let url = format!("https://api.github.com/repos/{GITHUB_REPO}/releases?per_page=100");
265    let client = reqwest::Client::new();
266    let resp = client
267        .get(&url)
268        .header("User-Agent", "ant-cli")
269        .header("Accept", "application/vnd.github+json")
270        .send()
271        .await
272        .map_err(|e| Error::UpdateFailed(format!("failed to fetch releases: {e}")))?;
273
274    if !resp.status().is_success() {
275        return Err(Error::UpdateFailed(format!(
276            "GitHub API returned status {} when fetching releases",
277            resp.status()
278        )));
279    }
280
281    let releases: Vec<serde_json::Value> = resp
282        .json()
283        .await
284        .map_err(|e| Error::UpdateFailed(format!("failed to parse releases JSON: {e}")))?;
285
286    // Find the newest stable ant-cli release by semver (skip drafts and pre-releases).
287    let mut best: Option<semver::Version> = None;
288    for release in &releases {
289        if release["draft"].as_bool().unwrap_or(false)
290            || release["prerelease"].as_bool().unwrap_or(false)
291        {
292            continue;
293        }
294        let tag = release["tag_name"].as_str().unwrap_or_default();
295        if let Some(version_str) = tag.strip_prefix(TAG_PREFIX) {
296            if let Ok(v) = semver::Version::parse(version_str) {
297                if best.as_ref().is_none_or(|b| v > *b) {
298                    best = Some(v);
299                }
300            }
301        }
302    }
303
304    best.map(|v| v.to_string())
305        .ok_or_else(|| Error::UpdateFailed("no ant-cli release found on GitHub".to_string()))
306}
307
308/// Download a release archive to a temp directory, streaming to disk.
309///
310/// Returns the path to the downloaded file and the raw bytes (needed for
311/// signature verification before extraction).
312async fn download_archive(
313    url: &str,
314    tmp_dir: &Path,
315    progress: &dyn ProgressReporter,
316) -> Result<(PathBuf, Vec<u8>)> {
317    progress.report_started(&format!("Downloading {CLI_BINARY_NAME} from {url}"));
318
319    let client = reqwest::Client::new();
320    let resp = client
321        .get(url)
322        .header("User-Agent", "ant-cli")
323        .send()
324        .await
325        .map_err(|e| Error::UpdateFailed(format!("download request failed: {e}")))?;
326
327    if !resp.status().is_success() {
328        return Err(Error::UpdateFailed(format!(
329            "download returned status {}",
330            resp.status()
331        )));
332    }
333
334    let total_size = resp.content_length().unwrap_or(0);
335    let mut downloaded: u64 = 0;
336
337    let tmp_archive = tmp_dir.join(".download.tmp");
338    let mut tmp_file = std::fs::File::create(&tmp_archive)
339        .map_err(|e| Error::UpdateFailed(format!("failed to create temp file: {e}")))?;
340
341    let mut stream = resp.bytes_stream();
342    while let Some(chunk) = stream.next().await {
343        let chunk =
344            chunk.map_err(|e| Error::UpdateFailed(format!("download stream error: {e}")))?;
345        downloaded += chunk.len() as u64;
346        std::io::Write::write_all(&mut tmp_file, &chunk)
347            .map_err(|e| Error::UpdateFailed(format!("failed to write temp file: {e}")))?;
348        progress.report_progress(downloaded, total_size);
349    }
350    drop(tmp_file);
351
352    progress.report_complete("Download complete");
353
354    let bytes = std::fs::read(&tmp_archive)
355        .map_err(|e| Error::UpdateFailed(format!("failed to read temp file: {e}")))?;
356
357    Ok((tmp_archive, bytes))
358}
359
360/// Download a small file (e.g., a `.sig` signature) into memory.
361async fn download_bytes(url: &str) -> Result<Vec<u8>> {
362    let client = reqwest::Client::new();
363    let resp = client
364        .get(url)
365        .header("User-Agent", "ant-cli")
366        .send()
367        .await
368        .map_err(|e| Error::UpdateFailed(format!("download request failed: {e}")))?;
369
370    if !resp.status().is_success() {
371        return Err(Error::UpdateFailed(format!(
372            "download returned status {} for {url}",
373            resp.status()
374        )));
375    }
376
377    resp.bytes()
378        .await
379        .map(|b| b.to_vec())
380        .map_err(|e| Error::UpdateFailed(format!("failed to read response body: {e}")))
381}
382
383/// Verify the ML-DSA-65 signature on an archive using the embedded release key.
384fn verify_signature(archive_bytes: &[u8], signature_bytes: &[u8]) -> Result<()> {
385    if signature_bytes.len() != SIGNATURE_SIZE {
386        return Err(Error::UpdateFailed(format!(
387            "invalid signature size: expected {SIGNATURE_SIZE}, got {}",
388            signature_bytes.len()
389        )));
390    }
391
392    let public_key = MlDsaPublicKey::from_bytes(MlDsaVariant::MlDsa65, RELEASE_SIGNING_KEY)
393        .map_err(|e| Error::UpdateFailed(format!("invalid embedded release key: {e}")))?;
394
395    let sig = MlDsaSignature::from_bytes(MlDsaVariant::MlDsa65, signature_bytes)
396        .map_err(|e| Error::UpdateFailed(format!("invalid signature format: {e}")))?;
397
398    let dsa = ml_dsa_65();
399    let valid = dsa
400        .verify_with_context(&public_key, archive_bytes, &sig, SIGNING_CONTEXT)
401        .map_err(|e| Error::UpdateFailed(format!("signature verification error: {e}")))?;
402
403    if valid {
404        Ok(())
405    } else {
406        Err(Error::UpdateFailed(
407            "signature verification failed: archive may be corrupted or tampered".to_string(),
408        ))
409    }
410}
411
412/// Extract the version string from a binary by running `<binary> --version`.
413async fn extract_version(binary_path: &Path) -> Result<String> {
414    let output = tokio::process::Command::new(binary_path)
415        .arg("--version")
416        .output()
417        .await
418        .map_err(|e| {
419            Error::UpdateFailed(format!(
420                "failed to run {} --version: {e}",
421                binary_path.display()
422            ))
423        })?;
424
425    if !output.status.success() {
426        return Err(Error::UpdateFailed(format!(
427            "{} --version exited with status {}",
428            binary_path.display(),
429            output.status
430        )));
431    }
432
433    let stdout = String::from_utf8_lossy(&output.stdout);
434    Ok(parse_version_from_stdout(&stdout).to_string())
435}
436
437/// Extract the version token from `--version` stdout.
438///
439/// Only the first line is inspected: historically `ant --version` was a single
440/// line like `ant 0.1.4`, but a richer multi-line long-version string ending
441/// with `License: MIT or Apache-2.0` shipped briefly and caused this parser —
442/// in 0.1.2 through 0.1.4 — to pick up `Apache-2.0` as the version. Scoping
443/// to the first line keeps us compatible with both forms.
444fn parse_version_from_stdout(stdout: &str) -> &str {
445    stdout
446        .lines()
447        .next()
448        .unwrap_or_default()
449        .split_whitespace()
450        .last()
451        .unwrap_or("unknown")
452}
453
454/// Replace the current executable with the new binary.
455///
456/// Uses the `self_replace` crate which handles platform-specific nuances,
457/// particularly on Windows where the running executable is locked.
458fn replace_binary(new_binary: &Path) -> Result<()> {
459    self_replace::self_replace(new_binary)
460        .map_err(|e| Error::UpdateFailed(format!("failed to replace binary: {e}")))?;
461    Ok(())
462}
463
464/// Build the download URL for a given version.
465///
466/// Tag format: `ant-cli-v{version}`
467/// Asset format: `ant-{version}-{target_triple}.{ext}`
468pub fn build_download_url(version: &str) -> Result<String> {
469    let asset_name = cli_platform_asset_name(version)?;
470    Ok(format!(
471        "https://github.com/{GITHUB_REPO}/releases/download/{TAG_PREFIX}{version}/{asset_name}"
472    ))
473}
474
475/// Returns the platform-specific archive asset name for the CLI binary.
476///
477/// Matches the naming convention from CI: `ant-{version}-{target_triple}.{ext}`
478fn cli_platform_asset_name(version: &str) -> Result<String> {
479    let target_triple = if cfg!(all(target_os = "linux", target_arch = "x86_64")) {
480        "x86_64-unknown-linux-musl"
481    } else if cfg!(all(target_os = "linux", target_arch = "aarch64")) {
482        "aarch64-unknown-linux-musl"
483    } else if cfg!(all(target_os = "macos", target_arch = "x86_64")) {
484        "x86_64-apple-darwin"
485    } else if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
486        "aarch64-apple-darwin"
487    } else if cfg!(all(target_os = "windows", target_arch = "x86_64")) {
488        "x86_64-pc-windows-msvc"
489    } else {
490        return Err(Error::UpdateFailed(format!(
491            "unsupported platform: {}-{}",
492            std::env::consts::OS,
493            std::env::consts::ARCH
494        )));
495    };
496
497    let ext = if cfg!(target_os = "windows") {
498        "zip"
499    } else {
500        "tar.gz"
501    };
502
503    Ok(format!("ant-{version}-{target_triple}.{ext}"))
504}
505
506fn parse_version(version: &str) -> Result<semver::Version> {
507    let cleaned = version.strip_prefix('v').unwrap_or(version);
508    semver::Version::parse(cleaned)
509        .map_err(|e| Error::UpdateFailed(format!("invalid version '{version}': {e}")))
510}
511
512#[cfg(test)]
513mod tests {
514    use super::*;
515
516    #[test]
517    fn parse_version_from_stdout_single_line() {
518        assert_eq!(parse_version_from_stdout("ant 0.1.4"), "0.1.4");
519        assert_eq!(parse_version_from_stdout("ant 0.1.4\n"), "0.1.4");
520    }
521
522    #[test]
523    fn parse_version_from_stdout_multi_line_license_trailer() {
524        // This is the exact shape that broke 0.1.2–0.1.4 self-update.
525        let stdout = "ant 0.1.4\n\
526            Autonomi network client\n\
527            \n\
528            Repository: https://github.com/WithAutonomi/ant-client\n\
529            License:    MIT or Apache-2.0\n";
530        assert_eq!(parse_version_from_stdout(stdout), "0.1.4");
531    }
532
533    #[test]
534    fn parse_version_from_stdout_empty() {
535        assert_eq!(parse_version_from_stdout(""), "unknown");
536    }
537
538    #[test]
539    fn parse_version_from_stdout_blank_first_line() {
540        assert_eq!(parse_version_from_stdout("\nant 0.1.4\n"), "unknown");
541    }
542
543    #[test]
544    fn parse_version_from_stdout_leading_whitespace() {
545        assert_eq!(parse_version_from_stdout("   ant 0.1.4\n"), "0.1.4");
546    }
547
548    #[test]
549    fn parse_version_valid() {
550        assert!(parse_version("1.2.3").is_ok());
551        assert!(parse_version("v1.2.3").is_ok());
552        assert!(parse_version("0.1.0").is_ok());
553    }
554
555    #[test]
556    fn parse_version_invalid() {
557        assert!(parse_version("not-a-version").is_err());
558        assert!(parse_version("").is_err());
559    }
560
561    #[test]
562    fn version_comparison() {
563        let v1 = parse_version("0.1.0").unwrap();
564        let v2 = parse_version("0.2.0").unwrap();
565        assert!(v2 > v1);
566
567        let v3 = parse_version("1.0.0").unwrap();
568        assert!(v3 > v2);
569
570        let same = parse_version("0.1.0").unwrap();
571        assert_eq!(v1, same);
572    }
573
574    #[test]
575    fn check_result_no_update() {
576        let check = UpdateCheck {
577            current_version: "1.0.0".to_string(),
578            latest_version: "1.0.0".to_string(),
579            update_available: false,
580            download_url: None,
581        };
582        assert!(!check.update_available);
583        assert!(check.download_url.is_none());
584    }
585
586    #[test]
587    fn check_result_with_update() {
588        let check = UpdateCheck {
589            current_version: "0.1.0".to_string(),
590            latest_version: "0.2.0".to_string(),
591            update_available: true,
592            download_url: Some("https://example.com/ant.tar.gz".to_string()),
593        };
594        assert!(check.update_available);
595        assert!(check.download_url.is_some());
596    }
597
598    #[test]
599    fn force_populates_download_url() {
600        let mut check = UpdateCheck {
601            current_version: "1.0.0".to_string(),
602            latest_version: "1.0.0".to_string(),
603            update_available: false,
604            download_url: None,
605        };
606        check.force().unwrap();
607        assert!(check.update_available);
608        assert!(check.download_url.is_some());
609    }
610
611    #[test]
612    fn platform_asset_name_format() {
613        let name = cli_platform_asset_name("1.2.3").unwrap();
614        assert!(name.starts_with("ant-1.2.3-"));
615        assert!(
616            name.ends_with(".tar.gz") || name.ends_with(".zip"),
617            "unexpected extension: {name}"
618        );
619    }
620
621    #[test]
622    fn build_download_url_format() {
623        let url = build_download_url("1.2.3").unwrap();
624        assert!(url.starts_with(
625            "https://github.com/WithAutonomi/ant-client/releases/download/ant-cli-v1.2.3/ant-1.2.3-"
626        ));
627        assert!(url.ends_with(".tar.gz") || url.ends_with(".zip"));
628    }
629
630    #[test]
631    fn update_check_serializes() {
632        let check = UpdateCheck {
633            current_version: "0.1.0".to_string(),
634            latest_version: "0.2.0".to_string(),
635            update_available: true,
636            download_url: Some("https://example.com/ant.tar.gz".to_string()),
637        };
638        let json = serde_json::to_string(&check).unwrap();
639        let deserialized: UpdateCheck = serde_json::from_str(&json).unwrap();
640        assert_eq!(deserialized.current_version, "0.1.0");
641        assert!(deserialized.update_available);
642    }
643
644    #[test]
645    fn verify_signature_rejects_wrong_size() {
646        let result = verify_signature(b"some archive data", &[0u8; 100]);
647        assert!(result.is_err());
648        let err = result.unwrap_err().to_string();
649        assert!(err.contains("invalid signature size"), "got: {err}");
650    }
651
652    #[test]
653    fn verify_signature_rejects_invalid_signature() {
654        let invalid_sig = vec![0u8; SIGNATURE_SIZE];
655        let result = verify_signature(b"some archive data", &invalid_sig);
656        assert!(result.is_err());
657    }
658
659    #[test]
660    fn verify_signature_valid_roundtrip() {
661        let dsa = ml_dsa_65();
662        let (public_key, secret_key) = dsa.generate_keypair().unwrap();
663        let archive = b"fake archive content for testing";
664
665        let sig = dsa
666            .sign_with_context(&secret_key, archive, SIGNING_CONTEXT)
667            .unwrap();
668
669        // Use the _with_key variant for testing (can't use embedded key)
670        let parsed_sig =
671            MlDsaSignature::from_bytes(MlDsaVariant::MlDsa65, &sig.to_bytes()).unwrap();
672        let valid = dsa
673            .verify_with_context(&public_key, archive, &parsed_sig, SIGNING_CONTEXT)
674            .unwrap();
675        assert!(valid);
676    }
677}