|
@@ -8,32 +8,33 @@ namespace Ppub {
|
|
|
INVALID_MEMBER,
|
|
|
INVALID_MEMBER_SIGNATURE,
|
|
|
INVALID_ID,
|
|
|
- INVALID_FORMAT
|
|
|
+ INVALID_FORMAT,
|
|
|
+ INVALID_SHARED_SIGNATURE,
|
|
|
+ INVALID_CREDENTIALS
|
|
|
}
|
|
|
|
|
|
public class Collection {
|
|
|
public uint8[] id { get; private set; }
|
|
|
public int serial { get; private set; }
|
|
|
+ public uint8[] shared_signature_key { get; private set; }
|
|
|
public Vector<string> domains { get; private set; }
|
|
|
public Vector<CollectionMember> members { get; private set; }
|
|
|
- public uint8[] auth { get; private set; }
|
|
|
+ public Vector<CollectionAgent> agents { get; private set; }
|
|
|
+ public uint8[] collection_signature { get; private set; }
|
|
|
+ public uint8[] shared_signature { get; private set; }
|
|
|
public Vector<CollectionPublication> publications { get; private set; }
|
|
|
public DateTime modified { get; private set; }
|
|
|
|
|
|
+ private string authoritative_section;
|
|
|
+ private string shared_section;
|
|
|
+
|
|
|
private void initialise() {
|
|
|
domains = new Vector<string>();
|
|
|
members = new Vector<CollectionMember>();
|
|
|
+ agents = new Vector<CollectionAgent>();
|
|
|
publications = new Vector<CollectionPublication>();
|
|
|
}
|
|
|
|
|
|
- public void increment_serial() {
|
|
|
- serial++;
|
|
|
- }
|
|
|
-
|
|
|
- public void touch() {
|
|
|
- modified = new DateTime.now_local();
|
|
|
- }
|
|
|
-
|
|
|
public Collection(out uint8[] private_signing_key) {
|
|
|
id = new uint8[Sodium.Asymmetric.Signing.PUBLIC_KEY_BYTES];
|
|
|
private_signing_key = new uint8[Sodium.Asymmetric.Signing.SECRET_KEY_BYTES];
|
|
@@ -43,6 +44,9 @@ namespace Ppub {
|
|
|
}
|
|
|
|
|
|
public Collection.from_stream(DataInputStream stream) throws Error {
|
|
|
+ authoritative_section = "";
|
|
|
+ shared_section = "";
|
|
|
+
|
|
|
var line = stream.read_line();
|
|
|
var signed_portion = line;
|
|
|
var header = line.split(" ");
|
|
@@ -74,14 +78,20 @@ namespace Ppub {
|
|
|
throw new CollectionError.INVALID_FORMAT(@"Malformed collection entry \"$(entry[0])\"");
|
|
|
}
|
|
|
|
|
|
+ if(entry[0] == "SSK") {
|
|
|
+ shared_signature_key = Base64.decode(entry[1]);
|
|
|
+ }
|
|
|
if(entry[0] == "MEM") {
|
|
|
members.add(new CollectionMember.from_string(entry[1]));
|
|
|
}
|
|
|
else if(entry[0] == "DOM") {
|
|
|
domains.add(entry[1]);
|
|
|
}
|
|
|
+ else if(entry[0] == "AGT") {
|
|
|
+ agents.add(new CollectionAgent.from_string(entry[1]));
|
|
|
+ }
|
|
|
else if(entry[0] == "SIG") {
|
|
|
- auth = Base64.decode(entry[1]);
|
|
|
+ collection_signature = Base64.decode(entry[1]);
|
|
|
break;
|
|
|
}
|
|
|
else {
|
|
@@ -91,7 +101,10 @@ namespace Ppub {
|
|
|
signed_portion += @"\n$line";
|
|
|
}
|
|
|
|
|
|
+ authoritative_section = @"$signed_portion\n$line";
|
|
|
+
|
|
|
line = stream.read_line();
|
|
|
+ shared_section += line;
|
|
|
var modified_entry = line.split(" ", 2);
|
|
|
if(modified_entry.length != 2 || modified_entry[0] != "MOD") {
|
|
|
throw new CollectionError.INVALID_FORMAT(@"Malformed or missing last modified date");
|
|
@@ -100,18 +113,19 @@ namespace Ppub {
|
|
|
modified = new DateTime.from_iso8601(modified_entry[1], null);
|
|
|
|
|
|
line = stream.read_line();
|
|
|
+ shared_section += @"\n$line";
|
|
|
if(line != "" && line != null) {
|
|
|
throw new CollectionError.INVALID_FORMAT(@"Line following MOD entry not blank");
|
|
|
}
|
|
|
|
|
|
- // Verify auth
|
|
|
+ // Verify authoratative section
|
|
|
var checksum = new BinaryData.from_byte_array(get_string_checksum(signed_portion));
|
|
|
- var signed_checksum = Sodium.Asymmetric.Signing.verify(auth, id);
|
|
|
+ var signed_checksum = Sodium.Asymmetric.Signing.verify(collection_signature, id);
|
|
|
if(signed_checksum == null) {
|
|
|
- throw new CollectionError.INVALID_COLLECTION_SIGNATURE("Could not verify signature portion of collection");
|
|
|
+ throw new CollectionError.INVALID_COLLECTION_SIGNATURE("Could not verify authoratative signature");
|
|
|
}
|
|
|
if(!checksum.equals(ate(signed_checksum))) {
|
|
|
- throw new CollectionError.INVALID_COLLECTION_SIGNATURE("Signature checksum does not match calculated checksum");
|
|
|
+ throw new CollectionError.INVALID_COLLECTION_SIGNATURE("Authoratative signature checksum does not match calculated checksum");
|
|
|
}
|
|
|
|
|
|
if(line == null) {
|
|
@@ -124,35 +138,85 @@ namespace Ppub {
|
|
|
break;
|
|
|
}
|
|
|
var entry = line.split(" ", 2);
|
|
|
-
|
|
|
+
|
|
|
if(entry[0] == "PUB") {
|
|
|
publications.add(new CollectionPublication.from_string(entry[1], members));
|
|
|
}
|
|
|
+ else if(entry[0] == "SSG") {
|
|
|
+ shared_signature = Base64.decode(entry[1]);
|
|
|
+ break;
|
|
|
+ }
|
|
|
else {
|
|
|
warning(@"Unrecognised entry type $(entry[0]), ignoring");
|
|
|
}
|
|
|
+ shared_section += @"\n$line";
|
|
|
+ }
|
|
|
+
|
|
|
+ if(shared_signature == null) {
|
|
|
+ throw new CollectionError.INVALID_SHARED_SIGNATURE("Shared signature entry missing");
|
|
|
+ }
|
|
|
+
|
|
|
+ // Verify shared signature
|
|
|
+ checksum = get_checksum();
|
|
|
+ signed_checksum = Sodium.Asymmetric.Signing.verify(shared_signature, shared_signature_key);
|
|
|
+ if(signed_checksum == null) {
|
|
|
+ throw new CollectionError.INVALID_SHARED_SIGNATURE("Could not verify shared signature");
|
|
|
+ }
|
|
|
+ if(!checksum.equals(ate(signed_checksum))) {
|
|
|
+ throw new CollectionError.INVALID_SHARED_SIGNATURE("Shared signature checksum does not match calculated checksum");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- public string to_string(uint8[] signing_key) {
|
|
|
- var strings = domains
|
|
|
- .select<string>(d => @"DOM $d")
|
|
|
- .concat(members.select<string>(m => m.to_string()));
|
|
|
- var data = strings.to_string(s => s, "\n");
|
|
|
- var pub_str = publications.to_string(p => p.to_string(), "\n");
|
|
|
+ public string authoratative_update(uint8[] authoratative_signing_key) {
|
|
|
+ serial++;
|
|
|
+ modified = new DateTime.now_local();
|
|
|
+
|
|
|
+ // Generate new collection signature/secret
|
|
|
+ var collection_secret = new uint8[Sodium.Asymmetric.Signing.SECRET_KEY_BYTES];
|
|
|
+ shared_signature_key = new uint8[Sodium.Asymmetric.Signing.PUBLIC_KEY_BYTES];
|
|
|
+ Sodium.Asymmetric.Signing.generate_keypair(shared_signature_key, collection_secret);
|
|
|
+
|
|
|
+ var strings = domains.select<string>(d => @"DOM $d").to_vector();
|
|
|
+
|
|
|
+ foreach (var agent in agents) {
|
|
|
+ agent.set_secret(collection_secret);
|
|
|
+ strings.add(agent.to_string());
|
|
|
+ }
|
|
|
|
|
|
- var signed_portion = @"PPCL $(Base64.encode(id)) $serial";
|
|
|
+ foreach (var member in members) {
|
|
|
+ member.set_secret(collection_secret);
|
|
|
+ strings.add(member.to_string());
|
|
|
+ }
|
|
|
+
|
|
|
+ var data = strings.to_string(s => s, "\n");
|
|
|
+ var signed_portion = @"PPCL $(Base64.encode(id)) $serial\nSSK $(Base64.encode(shared_signature_key))";
|
|
|
if(data != "") {
|
|
|
signed_portion += @"\n$data";
|
|
|
}
|
|
|
|
|
|
- auth = Sodium.Asymmetric.Signing.sign(get_string_checksum(signed_portion), signing_key);
|
|
|
-
|
|
|
- var full_str = @"$signed_portion\nSIG $(Base64.encode(auth))\nMOD $(modified.format_iso8601())\n";
|
|
|
+ collection_signature = Sodium.Asymmetric.Signing.sign(get_string_checksum(signed_portion), authoratative_signing_key);
|
|
|
+ authoritative_section = @"$signed_portion\nSIG $(Base64.encode(collection_signature))";
|
|
|
+ return member_update(collection_secret);
|
|
|
+ }
|
|
|
+
|
|
|
+ public string member_update(uint8[] collection_secret) {
|
|
|
+ modified = new DateTime.now_local();
|
|
|
+ var full_str = @"MOD $(modified.format_iso8601())\n";
|
|
|
+ var pub_str = publications.to_string(p => p.to_string(), "\n");
|
|
|
if(pub_str != "") {
|
|
|
- full_str += @"\n$pub_str\n";
|
|
|
+ full_str += @"\n$pub_str";
|
|
|
}
|
|
|
- return full_str;
|
|
|
+ shared_section = full_str;
|
|
|
+ shared_signature = Sodium.Asymmetric.Signing.sign(get_checksum().to_array(), collection_secret);
|
|
|
+ return to_string();
|
|
|
+ }
|
|
|
+
|
|
|
+ public string to_string() {
|
|
|
+ return @"$authoritative_section\n$shared_section\nSSG $(Base64.encode(shared_signature))";
|
|
|
+ }
|
|
|
+
|
|
|
+ private BinaryData get_checksum() {
|
|
|
+ return new BinaryData.from_byte_array(get_string_checksum(@"$authoritative_section\n$shared_section"));
|
|
|
}
|
|
|
|
|
|
internal static uint8[] get_string_checksum(string data) {
|
|
@@ -162,7 +226,8 @@ namespace Ppub {
|
|
|
var arr = signed_data.to_array();
|
|
|
print(@"ARR.length $(arr.length), str $data\n");
|
|
|
checksum_calculator.update(arr, arr.length);
|
|
|
- var checksum = new uint8[64];
|
|
|
+
|
|
|
+ var checksum = new uint8[ChecksumType.SHA512.get_length()];
|
|
|
size_t chk_len = checksum.length;
|
|
|
checksum_calculator.get_digest(checksum, ref chk_len);
|
|
|
checksum.length = (int)chk_len;
|
|
@@ -173,30 +238,109 @@ namespace Ppub {
|
|
|
|
|
|
public class CollectionMember {
|
|
|
public string name { get; private set; }
|
|
|
- public uint8[] public_key { get; private set; }
|
|
|
+ public uint8[] public_signing_key { get; private set; }
|
|
|
+ public uint8[] public_sealing_key { get; private set; }
|
|
|
+ public uint8[] collection_secret { get; private set; }
|
|
|
|
|
|
public string to_string() {
|
|
|
- return @"MEM $name $(Base64.encode(public_key))";
|
|
|
+ return @"MEM $name $(Base64.encode(public_signing_key)) $(Base64.encode(public_sealing_key)) $(Base64.encode(collection_secret))";
|
|
|
}
|
|
|
|
|
|
- public CollectionMember(string name, uint8[] public_key) {
|
|
|
+ public CollectionMember(string name, uint8[] public_signing_key, uint8[] public_sealing_key) {
|
|
|
this.name = name;
|
|
|
- this.public_key = public_key;
|
|
|
+ this.public_signing_key = public_signing_key;
|
|
|
+ this.public_sealing_key = public_sealing_key;
|
|
|
}
|
|
|
|
|
|
public CollectionMember.from_string(string line) throws CollectionError {
|
|
|
var parts = line.split(" ");
|
|
|
- if(parts.length < 2) {
|
|
|
- throw new CollectionError.INVALID_FORMAT("Member line must contain at least two fields, name and public key");
|
|
|
+ if(parts.length < 4) {
|
|
|
+ throw new CollectionError.INVALID_FORMAT("Member line must contain at least four fields, name, signing public key, sealing public key, and collection secret");
|
|
|
}
|
|
|
name = parts[0];
|
|
|
- public_key = Base64.decode(parts[1]);
|
|
|
+ public_signing_key = Base64.decode(parts[1]);
|
|
|
+ public_sealing_key = Base64.decode(parts[2]);
|
|
|
+ collection_secret = Base64.decode(parts[3]);
|
|
|
+ }
|
|
|
+
|
|
|
+ public void set_secret(uint8[] secret) {
|
|
|
+ collection_secret = Sodium.Asymmetric.Sealing.seal(secret, public_sealing_key);
|
|
|
+ }
|
|
|
+
|
|
|
+ public uint8[]? decrypt_collection_secret(CollectionMemberCredentials credentials) {
|
|
|
+ return Sodium.Asymmetric.Sealing.unseal(collection_secret, public_sealing_key, credentials.secret_sealing_key);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public class CollectionAgent {
|
|
|
+ public string name { get; private set; }
|
|
|
+ public uint8[] public_sealing_key { get; private set; }
|
|
|
+ public uint8[] collection_secret { get; private set; }
|
|
|
+
|
|
|
+ public string to_string() {
|
|
|
+ return @"AGT $name $(Base64.encode(public_sealing_key)) $(Base64.encode(collection_secret))";
|
|
|
+ }
|
|
|
+
|
|
|
+ public CollectionAgent(string name, uint8[] public_key) {
|
|
|
+ this.name = name;
|
|
|
+ this.public_sealing_key = public_key;
|
|
|
+ }
|
|
|
+
|
|
|
+ public CollectionAgent.from_string(string line) throws CollectionError {
|
|
|
+ var parts = line.split(" ");
|
|
|
+ if(parts.length < 3) {
|
|
|
+ throw new CollectionError.INVALID_FORMAT("Member line must contain at least two fields, name, public key, and collection secret");
|
|
|
+ }
|
|
|
+ name = parts[0];
|
|
|
+ public_sealing_key = Base64.decode(parts[1]);
|
|
|
+ collection_secret = Base64.decode(parts[2]);
|
|
|
+ }
|
|
|
+
|
|
|
+ public uint8[]? decrypt_collection_secret(uint8[] private_sealing_key) {
|
|
|
+ return Sodium.Asymmetric.Sealing.unseal(collection_secret, public_sealing_key, private_sealing_key);
|
|
|
+ }
|
|
|
+
|
|
|
+ public void set_secret(uint8[] secret) {
|
|
|
+ collection_secret = Sodium.Asymmetric.Sealing.seal(secret, public_sealing_key);
|
|
|
}
|
|
|
|
|
|
public static void new_keypair(out uint8[] private_key, out uint8[] public_key) {
|
|
|
- private_key = new uint8[Sodium.Asymmetric.Signing.SECRET_KEY_BYTES];
|
|
|
- public_key = new uint8[Sodium.Asymmetric.Signing.PUBLIC_KEY_BYTES];
|
|
|
- Sodium.Asymmetric.Signing.generate_keypair(public_key, private_key);
|
|
|
+ private_key = new uint8[Sodium.Asymmetric.Sealing.SECRET_KEY_BYTES];
|
|
|
+ public_key = new uint8[Sodium.Asymmetric.Sealing.PUBLIC_KEY_BYTES];
|
|
|
+ Sodium.Asymmetric.Sealing.generate_keypair(public_key, private_key);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public class CollectionMemberCredentials {
|
|
|
+ public uint8[] public_signing_key { get; private set; }
|
|
|
+ public uint8[] secret_signing_key { get; private set; }
|
|
|
+ public uint8[] public_sealing_key { get; private set; }
|
|
|
+ public uint8[] secret_sealing_key { get; private set; }
|
|
|
+
|
|
|
+ public CollectionMemberCredentials() {
|
|
|
+ public_signing_key = new uint8[Sodium.Asymmetric.Signing.PUBLIC_KEY_BYTES];
|
|
|
+ secret_signing_key = new uint8[Sodium.Asymmetric.Signing.SECRET_KEY_BYTES];
|
|
|
+ Sodium.Asymmetric.Signing.generate_keypair(public_signing_key, secret_signing_key);
|
|
|
+
|
|
|
+ public_sealing_key = new uint8[Sodium.Asymmetric.Sealing.PUBLIC_KEY_BYTES];
|
|
|
+ secret_sealing_key = new uint8[Sodium.Asymmetric.Sealing.SECRET_KEY_BYTES];
|
|
|
+ Sodium.Asymmetric.Sealing.generate_keypair(public_sealing_key, secret_sealing_key);
|
|
|
+ }
|
|
|
+
|
|
|
+ public string to_string() {
|
|
|
+ return @"$(Base64.encode(public_signing_key)):$(Base64.encode(public_sealing_key)):$(Base64.encode(secret_signing_key)):$(Base64.encode(secret_sealing_key))";
|
|
|
+ }
|
|
|
+
|
|
|
+ public CollectionMemberCredentials.from_string(string str) throws CollectionError {
|
|
|
+ var clean = str.chug().chomp();
|
|
|
+ var parts = clean.split(":");
|
|
|
+ if(parts.length < 4) {
|
|
|
+ throw new CollectionError.INVALID_CREDENTIALS("Invalid member credential format");
|
|
|
+ }
|
|
|
+ public_signing_key = Base64.decode(parts[0]);
|
|
|
+ public_sealing_key = Base64.decode(parts[1]);
|
|
|
+ secret_signing_key = Base64.decode(parts[2]);
|
|
|
+ secret_sealing_key = Base64.decode(parts[3]);
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -235,21 +379,21 @@ namespace Ppub {
|
|
|
return digest;
|
|
|
}
|
|
|
|
|
|
- public CollectionPublication(string file_name, DateTime publication_time, string member, uint8[] ppub_checksum, uint8[] signing_key) {
|
|
|
+ public CollectionPublication(string file_name, DateTime publication_time, string member, CollectionMemberCredentials member_credentials, uint8[] ppub_checksum) {
|
|
|
this.file_name = file_name;
|
|
|
this.publication_time = publication_time;
|
|
|
this.member = member;
|
|
|
this.publication_checksum = ppub_checksum;
|
|
|
- sign(signing_key);
|
|
|
+ sign(member_credentials);
|
|
|
}
|
|
|
|
|
|
- private void sign(uint8[] signing_key) {
|
|
|
+ private void sign(CollectionMemberCredentials member_credentials) {
|
|
|
var signed_portion = get_signed_portion();
|
|
|
var line_checksum = Collection.get_string_checksum(signed_portion);
|
|
|
var to_sign = new BinaryData.from_byte_array(line_checksum);
|
|
|
to_sign.append_byte_array(publication_checksum);
|
|
|
|
|
|
- signature = Sodium.Asymmetric.Signing.sign(to_sign.to_array(), signing_key);
|
|
|
+ signature = Sodium.Asymmetric.Signing.sign(to_sign.to_array(), member_credentials.secret_signing_key);
|
|
|
|
|
|
raw_line = @"PUB $signed_portion $(Base64.encode(signature))";
|
|
|
}
|
|
@@ -277,7 +421,7 @@ namespace Ppub {
|
|
|
|
|
|
var checksum = new BinaryData.from_byte_array(Collection.get_string_checksum(get_signed_portion()));
|
|
|
print(@"\t\t$(get_signed_portion()) == $(Base64.encode(checksum.to_array()))\n");
|
|
|
- var signature_data = Sodium.Asymmetric.Signing.verify(signature, collection_member.public_key);
|
|
|
+ var signature_data = Sodium.Asymmetric.Signing.verify(signature, collection_member.public_signing_key);
|
|
|
if(signature_data == null) {
|
|
|
throw new CollectionError.INVALID_COLLECTION_SIGNATURE("Invalid publication signature");
|
|
|
}
|