Skip to content

Commit 2da5c21

Browse files
original-el8ds41
andcommitted
Preserve the source license when signing a payload package
Signing replaces the publisher identity, and promotion copied the target's empty license fields over the source package's recorded license. The resulting package loaded without a license disclosure. Carry the source license name and link unless the target publisher supplies one. Promotion metadata changes only; payloads, executable bytes, and execution contracts are unchanged. Validation: 4 conversion promotion tests; 22 native publication and production signing tests. Fixes #81 Reported by @mark-yong in #81. Co-Authored-By: ds41 <noreply@invalid> Claude-Session: https://claude.com/claude-code/session/01U9WUEevf9AtdEw2XXLGZYY Design-Signed-Off-By: ds41; design-review=babb
1 parent 3267c05 commit 2da5c21

3 files changed

Lines changed: 104 additions & 3 deletions

File tree

‎docs/signing.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,9 @@ with `--development-publisher PATH`. The development private key is unnecessary.
106106
Signing a payload-format package verifies its source signatures and stored
107107
payloads, then signs its deployments and ranks under the production identity
108108
with a fresh package ID. Weight bytes and prepared execution contracts are
109-
preserved. Executable resources are authenticated, decoded, and re-encoded as
109+
preserved. The source package's recorded license name and link are preserved;
110+
signing changes the publisher identity, not the checkpoint's license terms.
111+
Executable resources are authenticated, decoded, and re-encoded as
110112
independent zstd level-3 frames; decoded bytes remain exact, while compressed
111113
frame bytes may change.
112114

‎helios/src/package/conversion.rs‎

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3206,12 +3206,32 @@ pub fn combine(request: &CombineRequest<'_>) -> Result<crate::package::export::P
32063206
package.payloads.clear();
32073207
package.deployments.clear();
32083208
if let Some(target) = request.target_publisher {
3209+
// The license describes the checkpoint, not the publisher. A promoted
3210+
// package keeps the source license unless the target names another.
3211+
let (source_license_name, source_license_link) = package
3212+
.publisher
3213+
.as_ref()
3214+
.map(|publisher| {
3215+
(
3216+
publisher.license_name.clone(),
3217+
publisher.license_link.clone(),
3218+
)
3219+
})
3220+
.unwrap_or_default();
32093221
package.publisher = Some(wire::PublisherMetadata {
32103222
namespace: target.namespace.clone(),
32113223
official_source: target.official_source.clone(),
32123224
development: target.development,
3213-
license_name: target.license_name.clone(),
3214-
license_link: target.license_link.clone(),
3225+
license_name: if target.license_name.is_empty() {
3226+
source_license_name
3227+
} else {
3228+
target.license_name.clone()
3229+
},
3230+
license_link: if target.license_link.is_empty() {
3231+
source_license_link
3232+
} else {
3233+
target.license_link.clone()
3234+
},
32153235
ranks: 0,
32163236
});
32173237
}
@@ -5049,6 +5069,7 @@ mod tests {
50495069
namespace: publisher.namespace.clone(),
50505070
official_source: "https://huggingface.co/example/model".into(),
50515071
license_name: "Example".into(),
5072+
license_link: "https://huggingface.co/example/model/LICENSE".into(),
50525073
..Default::default()
50535074
}),
50545075
frontend: Some(Default::default()),
@@ -7494,6 +7515,61 @@ mod tests {
74947515
payloads.verify().unwrap();
74957516
}
74967517

7518+
#[test]
7519+
fn promotion_inherits_the_source_license_when_the_target_omits_it() {
7520+
let scratch = Scratch::new("license-inherit");
7521+
let rng = SystemRandom::new();
7522+
let pkcs8 = signature::Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
7523+
let key = signature::Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
7524+
let public_key: [u8; 32] = key.public_key().as_ref().try_into().unwrap();
7525+
let publisher = Publisher {
7526+
name: "Example".into(),
7527+
namespace: "example".into(),
7528+
public_key,
7529+
development: false,
7530+
};
7531+
let matrix: Vec<u8> = (10..18).collect();
7532+
let full: Vec<u8> = (0..8).collect();
7533+
let source = publish_source(
7534+
&scratch,
7535+
"license-source",
7536+
1,
7537+
&[full.as_slice()],
7538+
&[matrix.as_slice()],
7539+
&key,
7540+
&publisher,
7541+
"model",
7542+
false,
7543+
);
7544+
let output = scratch.join("license-promoted");
7545+
let target = TargetPublisher {
7546+
namespace: "example".into(),
7547+
official_source: "https://huggingface.co/example/other".into(),
7548+
development: false,
7549+
license_name: String::new(),
7550+
license_link: String::new(),
7551+
};
7552+
promote(
7553+
&source,
7554+
&output,
7555+
std::slice::from_ref(&publisher),
7556+
"example",
7557+
"example",
7558+
Some(&target),
7559+
&key,
7560+
)
7561+
.unwrap();
7562+
let metadata = Metadata::open(&output).unwrap();
7563+
let envelope = protect::envelope(&metadata.envelope).unwrap();
7564+
let package = wire::PackageBody::decode(envelope.package_body.as_slice()).unwrap();
7565+
let publisher = package.publisher.unwrap();
7566+
assert_eq!(publisher.license_name, "Example");
7567+
assert_eq!(
7568+
publisher.license_link,
7569+
"https://huggingface.co/example/model/LICENSE"
7570+
);
7571+
}
7572+
74977573
#[test]
74987574
fn normalized_relative_falls_back_when_schema_rejects_container_paths() {
74997575
let identity = PayloadId::of(b"normalized-relative");

‎tests/preparation/test_native_publication.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,29 @@ def test_payload_promotion_preserves_rank_and_rebuild_metadata(tmp_path):
8686
assert after_deployment.rebuild.SerializeToString(deterministic=True) == index.rebuild.SerializeToString(deterministic=True)
8787

8888

89+
def test_payload_promotion_preserves_the_source_license(tmp_path):
90+
from dataclasses import replace
91+
from ignition.preparation.protection import License
92+
93+
link = 'https://huggingface.co/flare-tests/fixture/blob/main/LICENSE'
94+
source_signer = replace(publisher(), license=License('apache-2.0', link))
95+
source = publish(tmp_path, source_signer)
96+
target = publisher()
97+
promoted = tmp_path / 'promoted'
98+
payload_promote(
99+
source, promoted, publisher=target,
100+
profiles=[{'namespace': source_signer.namespace, 'development': False,
101+
'public_key': list(source_signer.public_key)}],
102+
accepted=source_signer.namespace, target_accepted=target.namespace,
103+
target_publisher=dict(namespace=target.namespace,
104+
official_source=target.official_source,
105+
development=False, license_name='', license_link=''))
106+
envelope = verify_signatures(promoted, Ed25519PublicKey.from_public_bytes(target.public_key))
107+
package = schema()('PackageBody').FromString(envelope.package_body)
108+
assert package.publisher.license_name == 'apache-2.0'
109+
assert package.publisher.license_link == link
110+
111+
89112
@pytest.mark.parametrize('operation', ['convert', 'sign'])
90113
def test_cli_rejects_legacy_publication_without_creating_output(tmp_path, operation):
91114
from types import SimpleNamespace

0 commit comments

Comments
 (0)