|
@@ -11,12 +11,13 @@ namespace Ppub {
|
|
INVALID_FORMAT,
|
|
INVALID_FORMAT,
|
|
INVALID_SHARED_SIGNATURE,
|
|
INVALID_SHARED_SIGNATURE,
|
|
INVALID_CREDENTIALS,
|
|
INVALID_CREDENTIALS,
|
|
- INVALID_URI
|
|
|
|
|
|
+ INVALID_URI,
|
|
|
|
+ INVALID_MEMBER_KEYS
|
|
}
|
|
}
|
|
|
|
|
|
public class Collection {
|
|
public class Collection {
|
|
public uint8[] id { get; private set; }
|
|
public uint8[] id { get; private set; }
|
|
- public string name { get; private set; }
|
|
|
|
|
|
+ public string name { get; set; }
|
|
public int serial { get; private set; }
|
|
public int serial { get; private set; }
|
|
public uint8[] shared_signature_key { get; private set; }
|
|
public uint8[] shared_signature_key { get; private set; }
|
|
public Vector<string> domains { get; private set; }
|
|
public Vector<string> domains { get; private set; }
|
|
@@ -46,7 +47,7 @@ namespace Ppub {
|
|
initialise();
|
|
initialise();
|
|
}
|
|
}
|
|
|
|
|
|
- public Collection.from_stream(DataInputStream stream) throws Error {
|
|
|
|
|
|
+ public Collection.from_stream(DataInputStream stream, bool verify = true) throws Error {
|
|
authoritative_section = "";
|
|
authoritative_section = "";
|
|
shared_section = "";
|
|
shared_section = "";
|
|
|
|
|
|
@@ -85,13 +86,16 @@ namespace Ppub {
|
|
shared_signature_key = Base64.decode(entry[1]);
|
|
shared_signature_key = Base64.decode(entry[1]);
|
|
}
|
|
}
|
|
else if(entry[0] == "MEM") {
|
|
else if(entry[0] == "MEM") {
|
|
- members.add(new CollectionMember.from_string(entry[1]));
|
|
|
|
|
|
+ members.add(new CollectionMember.from_string(entry[1], verify));
|
|
}
|
|
}
|
|
else if(entry[0] == "DOM") {
|
|
else if(entry[0] == "DOM") {
|
|
domains.add(entry[1]);
|
|
domains.add(entry[1]);
|
|
}
|
|
}
|
|
else if(entry[0] == "AGT") {
|
|
else if(entry[0] == "AGT") {
|
|
- agents.add(new CollectionAgent.from_string(entry[1]));
|
|
|
|
|
|
+ agents.add(new CollectionAgent.from_string(entry[1], verify));
|
|
|
|
+ }
|
|
|
|
+ else if(entry[0] == "NAM") {
|
|
|
|
+ name = entry[1];
|
|
}
|
|
}
|
|
else if(entry[0] == "SIG") {
|
|
else if(entry[0] == "SIG") {
|
|
collection_signature = Base64.decode(entry[1]);
|
|
collection_signature = Base64.decode(entry[1]);
|
|
@@ -130,13 +134,15 @@ namespace Ppub {
|
|
}
|
|
}
|
|
|
|
|
|
// Verify authoratative section
|
|
// Verify authoratative section
|
|
- var checksum = new BinaryData.from_byte_array(get_string_checksum(signed_portion));
|
|
|
|
- var signed_checksum = Sodium.Asymmetric.Signing.verify(collection_signature, id);
|
|
|
|
- if(signed_checksum == null) {
|
|
|
|
- throw new CollectionError.INVALID_COLLECTION_SIGNATURE("Could not verify authoratative signature");
|
|
|
|
- }
|
|
|
|
- if(!checksum.equals(ate(signed_checksum))) {
|
|
|
|
- throw new CollectionError.INVALID_COLLECTION_SIGNATURE("Authoratative signature checksum does not match calculated checksum");
|
|
|
|
|
|
+ if(verify) {
|
|
|
|
+ var checksum = new BinaryData.from_byte_array(get_string_checksum(signed_portion));
|
|
|
|
+ var signed_checksum = Sodium.Asymmetric.Signing.verify(collection_signature, id);
|
|
|
|
+ if(signed_checksum == null) {
|
|
|
|
+ throw new CollectionError.INVALID_COLLECTION_SIGNATURE("Could not verify authoratative signature");
|
|
|
|
+ }
|
|
|
|
+ if(!checksum.equals(ate(signed_checksum))) {
|
|
|
|
+ throw new CollectionError.INVALID_COLLECTION_SIGNATURE("Authoratative signature checksum does not match calculated checksum");
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
|
|
if(line == null) {
|
|
if(line == null) {
|
|
@@ -168,13 +174,15 @@ namespace Ppub {
|
|
}
|
|
}
|
|
|
|
|
|
// Verify shared signature
|
|
// 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");
|
|
|
|
|
|
+ if(verify) {
|
|
|
|
+ var checksum = get_checksum();
|
|
|
|
+ var 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");
|
|
|
|
+ }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
@@ -205,6 +213,9 @@ namespace Ppub {
|
|
|
|
|
|
var data = strings.to_string(s => s, "\n");
|
|
var data = strings.to_string(s => s, "\n");
|
|
var signed_portion = @"PPCL $(Base64.encode(id)) $serial\nSSK $(Base64.encode(shared_signature_key))";
|
|
var signed_portion = @"PPCL $(Base64.encode(id)) $serial\nSSK $(Base64.encode(shared_signature_key))";
|
|
|
|
+ if(name != null && name.length > 0) {
|
|
|
|
+ signed_portion += @"\nNAM $name";
|
|
|
|
+ }
|
|
if(data != "") {
|
|
if(data != "") {
|
|
signed_portion += @"\n$data";
|
|
signed_portion += @"\n$data";
|
|
}
|
|
}
|
|
@@ -255,37 +266,40 @@ namespace Ppub {
|
|
|
|
|
|
public class CollectionMember {
|
|
public class CollectionMember {
|
|
public string name { get; private set; }
|
|
public string name { get; private set; }
|
|
- public uint8[] public_signing_key { get; private set; }
|
|
|
|
- public uint8[] public_sealing_key { get; private set; }
|
|
|
|
|
|
+ public CollectionMemberPublicKeys public_keys { get; private set; }
|
|
public uint8[] collection_secret { get; private set; }
|
|
public uint8[] collection_secret { get; private set; }
|
|
|
|
|
|
public string to_string() {
|
|
public string to_string() {
|
|
- return @"MEM $name $(Base64.encode(public_signing_key)) $(Base64.encode(public_sealing_key)) $(Base64.encode(collection_secret))";
|
|
|
|
|
|
+ return @"MEM $name $public_keys $(Base64.encode(collection_secret))";
|
|
}
|
|
}
|
|
|
|
|
|
- public CollectionMember(string name, uint8[] public_signing_key, uint8[] public_sealing_key) {
|
|
|
|
|
|
+ public CollectionMember(string name, CollectionMemberPublicKeys keys) {
|
|
this.name = name;
|
|
this.name = name;
|
|
- this.public_signing_key = public_signing_key;
|
|
|
|
- this.public_sealing_key = public_sealing_key;
|
|
|
|
|
|
+ public_keys = keys;
|
|
}
|
|
}
|
|
|
|
|
|
- public CollectionMember.from_string(string line) throws CollectionError {
|
|
|
|
|
|
+ public CollectionMember.from_string(string line, bool strict = true) throws CollectionError {
|
|
var parts = line.split(" ");
|
|
var parts = line.split(" ");
|
|
- 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");
|
|
|
|
|
|
+ if(parts.length < 3 && strict) {
|
|
|
|
+ throw new CollectionError.INVALID_FORMAT("Member line must contain at least three fields, name, public key, and collection secret");
|
|
|
|
+ }
|
|
|
|
+ if(parts.length < 2 && !strict) {
|
|
|
|
+ throw new CollectionError.INVALID_FORMAT("Member line must contain at least two fields, name, public key, and optionally collection secret");
|
|
}
|
|
}
|
|
name = parts[0];
|
|
name = parts[0];
|
|
- public_signing_key = Base64.decode(parts[1]);
|
|
|
|
- public_sealing_key = Base64.decode(parts[2]);
|
|
|
|
- collection_secret = Base64.decode(parts[3]);
|
|
|
|
|
|
+ public_keys = new CollectionMemberPublicKeys.from_string(parts[1]);
|
|
|
|
+
|
|
|
|
+ if(parts.length > 2) {
|
|
|
|
+ collection_secret = Base64.decode(parts[2]);
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
|
|
public void set_secret(uint8[] secret) {
|
|
public void set_secret(uint8[] secret) {
|
|
- collection_secret = Sodium.Asymmetric.Sealing.seal(secret, public_sealing_key);
|
|
|
|
|
|
+ collection_secret = Sodium.Asymmetric.Sealing.seal(secret, public_keys.sealing_key);
|
|
}
|
|
}
|
|
|
|
|
|
public uint8[]? decrypt_collection_secret(CollectionMemberCredentials credentials) {
|
|
public uint8[]? decrypt_collection_secret(CollectionMemberCredentials credentials) {
|
|
- return Sodium.Asymmetric.Sealing.unseal(collection_secret, public_sealing_key, credentials.secret_sealing_key);
|
|
|
|
|
|
+ return Sodium.Asymmetric.Sealing.unseal(collection_secret, public_keys.sealing_key, credentials.secret_sealing_key);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
@@ -303,14 +317,20 @@ namespace Ppub {
|
|
this.public_sealing_key = public_key;
|
|
this.public_sealing_key = public_key;
|
|
}
|
|
}
|
|
|
|
|
|
- public CollectionAgent.from_string(string line) throws CollectionError {
|
|
|
|
|
|
+ public CollectionAgent.from_string(string line, bool strict = true) throws CollectionError {
|
|
var parts = line.split(" ");
|
|
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");
|
|
|
|
|
|
+ if(parts.length < 3 && strict) {
|
|
|
|
+ throw new CollectionError.INVALID_FORMAT("Agent line must contain at least three fields, name, public key, and collection secret");
|
|
|
|
+ }
|
|
|
|
+ if(parts.length < 2 && !strict) {
|
|
|
|
+ throw new CollectionError.INVALID_FORMAT("Agent line must contain at least two fields, name, public key, and optionally collection secret");
|
|
}
|
|
}
|
|
name = parts[0];
|
|
name = parts[0];
|
|
public_sealing_key = Base64.decode(parts[1]);
|
|
public_sealing_key = Base64.decode(parts[1]);
|
|
- collection_secret = Base64.decode(parts[2]);
|
|
|
|
|
|
+
|
|
|
|
+ if(parts.length > 2) {
|
|
|
|
+ collection_secret = Base64.decode(parts[2]);
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
|
|
public uint8[]? decrypt_collection_secret(uint8[] private_sealing_key) {
|
|
public uint8[]? decrypt_collection_secret(uint8[] private_sealing_key) {
|
|
@@ -328,6 +348,36 @@ namespace Ppub {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ public class CollectionMemberPublicKeys {
|
|
|
|
+ public uint8[] signing_key { get; private set; }
|
|
|
|
+ public uint8[] sealing_key { get; private set; }
|
|
|
|
+
|
|
|
|
+ public CollectionMemberPublicKeys(uint8[] sign_key, uint8[] seal_key) {
|
|
|
|
+ signing_key = sign_key;
|
|
|
|
+ sealing_key = seal_key;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public string to_string() {
|
|
|
|
+ var combined = new BinaryData.from_byte_array(signing_key);
|
|
|
|
+ combined.append_byte_array(sealing_key);
|
|
|
|
+ return @"CLMPK:$(Base64.encode(combined.to_array()))";
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public CollectionMemberPublicKeys.from_string(string str) throws CollectionError {
|
|
|
|
+ var parts = str.split(":", 2);
|
|
|
|
+ if(parts.length < 2 || parts[0] != "CLMPK") {
|
|
|
|
+ throw new CollectionError.INVALID_MEMBER_KEYS("Malformed CLMPK string");
|
|
|
|
+ }
|
|
|
|
+ var data = Base64.decode(parts[1]);
|
|
|
|
+ if(data.length != Sodium.Asymmetric.Signing.PUBLIC_KEY_BYTES + Sodium.Asymmetric.Sealing.PUBLIC_KEY_BYTES) {
|
|
|
|
+ throw new CollectionError.INVALID_MEMBER_KEYS("Wrong sized CLMPK string");
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ signing_key = data[0:Sodium.Asymmetric.Signing.PUBLIC_KEY_BYTES];
|
|
|
|
+ sealing_key = data[Sodium.Asymmetric.Signing.PUBLIC_KEY_BYTES:Sodium.Asymmetric.Signing.SECRET_KEY_BYTES];
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
public class CollectionMemberCredentials {
|
|
public class CollectionMemberCredentials {
|
|
public uint8[] public_signing_key { get; private set; }
|
|
public uint8[] public_signing_key { get; private set; }
|
|
public uint8[] secret_signing_key { get; private set; }
|
|
public uint8[] secret_signing_key { get; private set; }
|
|
@@ -345,19 +395,43 @@ namespace Ppub {
|
|
}
|
|
}
|
|
|
|
|
|
public string to_string() {
|
|
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))";
|
|
|
|
|
|
+ var str = "PPCLMC\n";
|
|
|
|
+ str += @"PKSIG $(Base64.encode(public_signing_key))\n";
|
|
|
|
+ str += @"PKENC $(Base64.encode(public_sealing_key))\n";
|
|
|
|
+ str += @"SKSIG $(Base64.encode(secret_signing_key))\n";
|
|
|
|
+ str += @"SKENC $(Base64.encode(secret_sealing_key))";
|
|
|
|
+ return str;
|
|
}
|
|
}
|
|
|
|
|
|
public CollectionMemberCredentials.from_string(string str) throws CollectionError {
|
|
public CollectionMemberCredentials.from_string(string str) throws CollectionError {
|
|
var clean = str.chug().chomp();
|
|
var clean = str.chug().chomp();
|
|
- var parts = clean.split(":");
|
|
|
|
- if(parts.length < 4) {
|
|
|
|
|
|
+ var lines = clean.split("\n", 6);
|
|
|
|
+ if(lines.length < 5 || lines[0] != "PPCLMC") {
|
|
throw new CollectionError.INVALID_CREDENTIALS("Invalid member credential format");
|
|
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]);
|
|
|
|
|
|
+
|
|
|
|
+ foreach (var line in lines) {
|
|
|
|
+ var parts = line.split(" ", 2);
|
|
|
|
+ if(lines.length < 2) {
|
|
|
|
+ throw new CollectionError.INVALID_CREDENTIALS("Invalid member credential entry");
|
|
|
|
+ }
|
|
|
|
+ if(parts[0] == "PKSIG") {
|
|
|
|
+ public_signing_key = Base64.decode(parts[1]);
|
|
|
|
+ }
|
|
|
|
+ if(parts[0] == "PKENC") {
|
|
|
|
+ public_sealing_key = Base64.decode(parts[1]);
|
|
|
|
+ }
|
|
|
|
+ if(parts[0] == "SKSIG") {
|
|
|
|
+ secret_signing_key = Base64.decode(parts[1]);
|
|
|
|
+ }
|
|
|
|
+ if(parts[0] == "SKENC") {
|
|
|
|
+ secret_sealing_key = Base64.decode(parts[1]);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public CollectionMemberPublicKeys get_public_keys() {
|
|
|
|
+ return new CollectionMemberPublicKeys(public_signing_key, public_sealing_key);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
@@ -437,7 +511,7 @@ namespace Ppub {
|
|
}
|
|
}
|
|
|
|
|
|
var checksum = new BinaryData.from_byte_array(Collection.get_string_checksum(get_signed_portion()));
|
|
var checksum = new BinaryData.from_byte_array(Collection.get_string_checksum(get_signed_portion()));
|
|
- var signature_data = Sodium.Asymmetric.Signing.verify(signature, collection_member.public_signing_key);
|
|
|
|
|
|
+ var signature_data = Sodium.Asymmetric.Signing.verify(signature, collection_member.public_keys.signing_key);
|
|
if(signature_data == null) {
|
|
if(signature_data == null) {
|
|
throw new CollectionError.INVALID_COLLECTION_SIGNATURE("Invalid publication signature");
|
|
throw new CollectionError.INVALID_COLLECTION_SIGNATURE("Invalid publication signature");
|
|
}
|
|
}
|
|
@@ -534,6 +608,10 @@ namespace Ppub {
|
|
col_id += "=";
|
|
col_id += "=";
|
|
collection_id = Base64.decode(col_id);
|
|
collection_id = Base64.decode(col_id);
|
|
|
|
|
|
|
|
+ if(collection_id.length != Sodium.Asymmetric.Signing.PUBLIC_KEY_BYTES) {
|
|
|
|
+ throw new CollectionError.INVALID_URI("Invalid collection id");
|
|
|
|
+ }
|
|
|
|
+
|
|
var path = uri.get_path();
|
|
var path = uri.get_path();
|
|
var parts = path.split("/", 2);
|
|
var parts = path.split("/", 2);
|
|
if(parts[0] != "") {
|
|
if(parts[0] != "") {
|