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 mut cmd = tokio::process::Command::new(binary_path);
415    cmd.arg("--version");
416    // CREATE_NO_WINDOW: prevents Windows from allocating a console window for
417    // the console-subsystem child binary. Without this, every version probe
418    // flashes a window — visible as "ghost flashes" in GUI consumers.
419    #[cfg(windows)]
420    {
421        const CREATE_NO_WINDOW: u32 = 0x08000000;
422        cmd.creation_flags(CREATE_NO_WINDOW);
423    }
424    let output = cmd.output().await.map_err(|e| {
425        Error::UpdateFailed(format!(
426            "failed to run {} --version: {e}",
427            binary_path.display()
428        ))
429    })?;
430
431    if !output.status.success() {
432        return Err(Error::UpdateFailed(format!(
433            "{} --version exited with status {}",
434            binary_path.display(),
435            output.status
436        )));
437    }
438
439    let stdout = String::from_utf8_lossy(&output.stdout);
440    Ok(parse_version_from_stdout(&stdout).to_string())
441}
442
443/// Extract the version token from `--version` stdout.
444///
445/// Only the first line is inspected: historically `ant --version` was a single
446/// line like `ant 0.1.4`, but a richer multi-line long-version string ending
447/// with `License: MIT or Apache-2.0` shipped briefly and caused this parser —
448/// in 0.1.2 through 0.1.4 — to pick up `Apache-2.0` as the version. Scoping
449/// to the first line keeps us compatible with both forms.
450fn parse_version_from_stdout(stdout: &str) -> &str {
451    stdout
452        .lines()
453        .next()
454        .unwrap_or_default()
455        .split_whitespace()
456        .last()
457        .unwrap_or("unknown")
458}
459
460/// Replace the current executable with the new binary.
461///
462/// Uses the `self_replace` crate which handles platform-specific nuances,
463/// particularly on Windows where the running executable is locked.
464fn replace_binary(new_binary: &Path) -> Result<()> {
465    self_replace::self_replace(new_binary)
466        .map_err(|e| Error::UpdateFailed(format!("failed to replace binary: {e}")))?;
467    Ok(())
468}
469
470/// Build the download URL for a given version.
471///
472/// Tag format: `ant-cli-v{version}`
473/// Asset format: `ant-{version}-{target_triple}.{ext}`
474pub fn build_download_url(version: &str) -> Result<String> {
475    let asset_name = cli_platform_asset_name(version)?;
476    Ok(format!(
477        "https://github.com/{GITHUB_REPO}/releases/download/{TAG_PREFIX}{version}/{asset_name}"
478    ))
479}
480
481/// Returns the platform-specific archive asset name for the CLI binary.
482///
483/// Matches the naming convention from CI: `ant-{version}-{target_triple}.{ext}`
484fn cli_platform_asset_name(version: &str) -> Result<String> {
485    let target_triple = if cfg!(all(target_os = "linux", target_arch = "x86_64")) {
486        "x86_64-unknown-linux-musl"
487    } else if cfg!(all(target_os = "linux", target_arch = "aarch64")) {
488        "aarch64-unknown-linux-musl"
489    } else if cfg!(all(target_os = "macos", target_arch = "x86_64")) {
490        "x86_64-apple-darwin"
491    } else if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
492        "aarch64-apple-darwin"
493    } else if cfg!(all(target_os = "windows", target_arch = "x86_64")) {
494        "x86_64-pc-windows-msvc"
495    } else {
496        return Err(Error::UpdateFailed(format!(
497            "unsupported platform: {}-{}",
498            std::env::consts::OS,
499            std::env::consts::ARCH
500        )));
501    };
502
503    let ext = if cfg!(target_os = "windows") {
504        "zip"
505    } else {
506        "tar.gz"
507    };
508
509    Ok(format!("ant-{version}-{target_triple}.{ext}"))
510}
511
512fn parse_version(version: &str) -> Result<semver::Version> {
513    let cleaned = version.strip_prefix('v').unwrap_or(version);
514    semver::Version::parse(cleaned)
515        .map_err(|e| Error::UpdateFailed(format!("invalid version '{version}': {e}")))
516}
517
518#[cfg(test)]
519mod tests {
520    use super::*;
521
522    #[test]
523    fn parse_version_from_stdout_single_line() {
524        assert_eq!(parse_version_from_stdout("ant 0.1.4"), "0.1.4");
525        assert_eq!(parse_version_from_stdout("ant 0.1.4\n"), "0.1.4");
526    }
527
528    #[test]
529    fn parse_version_from_stdout_multi_line_license_trailer() {
530        // This is the exact shape that broke 0.1.2–0.1.4 self-update.
531        let stdout = "ant 0.1.4\n\
532            Autonomi network client\n\
533            \n\
534            Repository: https://github.com/WithAutonomi/ant-client\n\
535            License:    MIT or Apache-2.0\n";
536        assert_eq!(parse_version_from_stdout(stdout), "0.1.4");
537    }
538
539    #[test]
540    fn parse_version_from_stdout_empty() {
541        assert_eq!(parse_version_from_stdout(""), "unknown");
542    }
543
544    #[test]
545    fn parse_version_from_stdout_blank_first_line() {
546        assert_eq!(parse_version_from_stdout("\nant 0.1.4\n"), "unknown");
547    }
548
549    #[test]
550    fn parse_version_from_stdout_leading_whitespace() {
551        assert_eq!(parse_version_from_stdout("   ant 0.1.4\n"), "0.1.4");
552    }
553
554    #[test]
555    fn parse_version_valid() {
556        assert!(parse_version("1.2.3").is_ok());
557        assert!(parse_version("v1.2.3").is_ok());
558        assert!(parse_version("0.1.0").is_ok());
559    }
560
561    #[test]
562    fn parse_version_invalid() {
563        assert!(parse_version("not-a-version").is_err());
564        assert!(parse_version("").is_err());
565    }
566
567    #[test]
568    fn version_comparison() {
569        let v1 = parse_version("0.1.0").unwrap();
570        let v2 = parse_version("0.2.0").unwrap();
571        assert!(v2 > v1);
572
573        let v3 = parse_version("1.0.0").unwrap();
574        assert!(v3 > v2);
575
576        let same = parse_version("0.1.0").unwrap();
577        assert_eq!(v1, same);
578    }
579
580    #[test]
581    fn check_result_no_update() {
582        let check = UpdateCheck {
583            current_version: "1.0.0".to_string(),
584            latest_version: "1.0.0".to_string(),
585            update_available: false,
586            download_url: None,
587        };
588        assert!(!check.update_available);
589        assert!(check.download_url.is_none());
590    }
591
592    #[test]
593    fn check_result_with_update() {
594        let check = UpdateCheck {
595            current_version: "0.1.0".to_string(),
596            latest_version: "0.2.0".to_string(),
597            update_available: true,
598            download_url: Some("https://example.com/ant.tar.gz".to_string()),
599        };
600        assert!(check.update_available);
601        assert!(check.download_url.is_some());
602    }
603
604    #[test]
605    fn force_populates_download_url() {
606        let mut check = UpdateCheck {
607            current_version: "1.0.0".to_string(),
608            latest_version: "1.0.0".to_string(),
609            update_available: false,
610            download_url: None,
611        };
612        check.force().unwrap();
613        assert!(check.update_available);
614        assert!(check.download_url.is_some());
615    }
616
617    #[test]
618    fn platform_asset_name_format() {
619        let name = cli_platform_asset_name("1.2.3").unwrap();
620        assert!(name.starts_with("ant-1.2.3-"));
621        assert!(
622            name.ends_with(".tar.gz") || name.ends_with(".zip"),
623            "unexpected extension: {name}"
624        );
625    }
626
627    #[test]
628    fn build_download_url_format() {
629        let url = build_download_url("1.2.3").unwrap();
630        assert!(url.starts_with(
631            "https://github.com/WithAutonomi/ant-client/releases/download/ant-cli-v1.2.3/ant-1.2.3-"
632        ));
633        assert!(url.ends_with(".tar.gz") || url.ends_with(".zip"));
634    }
635
636    #[test]
637    fn update_check_serializes() {
638        let check = UpdateCheck {
639            current_version: "0.1.0".to_string(),
640            latest_version: "0.2.0".to_string(),
641            update_available: true,
642            download_url: Some("https://example.com/ant.tar.gz".to_string()),
643        };
644        let json = serde_json::to_string(&check).unwrap();
645        let deserialized: UpdateCheck = serde_json::from_str(&json).unwrap();
646        assert_eq!(deserialized.current_version, "0.1.0");
647        assert!(deserialized.update_available);
648    }
649
650    #[test]
651    fn verify_signature_rejects_wrong_size() {
652        let result = verify_signature(b"some archive data", &[0u8; 100]);
653        assert!(result.is_err());
654        let err = result.unwrap_err().to_string();
655        assert!(err.contains("invalid signature size"), "got: {err}");
656    }
657
658    #[test]
659    fn verify_signature_rejects_invalid_signature() {
660        let invalid_sig = vec![0u8; SIGNATURE_SIZE];
661        let result = verify_signature(b"some archive data", &invalid_sig);
662        assert!(result.is_err());
663    }
664
665    #[test]
666    fn verify_signature_valid_roundtrip() {
667        let dsa = ml_dsa_65();
668        let (public_key, secret_key) = dsa.generate_keypair().unwrap();
669        let archive = b"fake archive content for testing";
670
671        let sig = dsa
672            .sign_with_context(&secret_key, archive, SIGNING_CONTEXT)
673            .unwrap();
674
675        // Use the _with_key variant for testing (can't use embedded key)
676        let parsed_sig =
677            MlDsaSignature::from_bytes(MlDsaVariant::MlDsa65, &sig.to_bytes()).unwrap();
678        let valid = dsa
679            .verify_with_context(&public_key, archive, &parsed_sig, SIGNING_CONTEXT)
680            .unwrap();
681        assert!(valid);
682    }
683}