Skip to content

Commit

Permalink
Merge pull request #20 from paragonie/additional-freedom
Browse files Browse the repository at this point in the history
Additional Degree of Freedom: Build Artifact Type
  • Loading branch information
paragonie-security authored Aug 12, 2021
2 parents c5ff6c8 + 504a568 commit 7132082
Show file tree
Hide file tree
Showing 15 changed files with 207 additions and 67 deletions.
12 changes: 7 additions & 5 deletions docs/reference/DbInterface.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,10 @@ Appends signature/etc. information about a software update.
2. `string` - Package
3. `string` - Public Key
4. `string` - Release (version)
5. `string` - Signature (of the release file)
6. `array` - Metadata
7. `string` - Hash
5. `?string` - Artifact
6. `string` - Signature (of the release file)
7. `array` - Metadata
8. `string` - Hash

**Returns** a `bool`.

Expand All @@ -90,8 +91,9 @@ Revoke an existing update.
2. `string` - Package
3. `string` - Public Key
4. `string` - Release (version)
5. `array` - Metadata
6. `string` - Hash
5. `?string` - Artifact
6. `array` - Metadata
7. `string` - Hash

**Returns** a `bool`.

Expand Down
6 changes: 6 additions & 0 deletions docs/specification/Protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ An `AppendUpdate` action **MUST** contain the following fields:
| `signature` | Signature of the update file. |
| `package` | Name of the package (owned by provider). |
| `release` | Version being released. |
| `artifact` | Build artifact type being released. |

An `AppendUpdate` action **MAY ALSO** Contain the following optional fields:

Expand All @@ -175,6 +176,9 @@ correct provider (or the Super Provider, if applicable).

When this action is performed, it should insert a new row in a database.

The `artifact` is optional. It can be used to distinguish different deliverables for different
platforms or distribution channels (e.g. for `.zip` files vs `.patch` files).

### RevokeUpdate

A `RevokeUpdate` action **MUST** contain the following fields:
Expand All @@ -186,6 +190,7 @@ A `RevokeUpdate` action **MUST** contain the following fields:
| `public-key` | Base64url-encoded verification key. |
| `package` | Name of the package (owned by provider). |
| `release` | Version being released. |
| `artifact` | Which build artifact type to revoke. |

If the `provider` is not found in the local key store when an `RevokeUpdate`
action is encountered, an error **MUST** be raised. In most languages, this
Expand All @@ -207,6 +212,7 @@ A `AttestUpdate` action **MUST** contain the following fields:
| `provider` | Provider name. |
| `package` | Name of the package (owned by provider). |
| `release` | Version of the package in question. |
| `artifact` | Which build artifact type to attest. |
| `attestor` | Provider attesting this package. |
| `attestation` | See below. |

Expand Down
5 changes: 3 additions & 2 deletions lib/Client/GossamerClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,12 @@ public function getVerificationKeys($provider, $purpose = null)
* @param string $provider
* @param string $package
* @param string $version
* @param ?string $artifact
* @return UpdateFile
*/
private function getUpdateActual($provider, $package, $version)
private function getUpdateActual($provider, $package, $version, $artifact = null)
{
return $this->trust->getUpdateInfo($provider, $package, $version)
return $this->trust->getUpdateInfo($provider, $package, $version, $artifact)
->setAlgorithm($this->alg)
->setAttestPolicy($this->policy);
}
Expand Down
33 changes: 26 additions & 7 deletions lib/Client/TrustMode/FederatedTrust.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,14 @@ public function getVerificationKeys($provider, $purpose = null)
* @param string $provider
* @param string $package
* @param string $version
* @param ?string $artifact
* @return UpdateFile
* @throws GossamerException
*/
public function getUpdateInfo($provider, $package, $version)
public function getUpdateInfo($provider, $package, $version, $artifact = null)
{
/** @var array{publickey: string, signature: string, metadata:array} $data */
$data = $this->getUpdateInfoHttp($provider, $package, $version);
$data = $this->getUpdateInfoHttp($provider, $package, $version, $artifact);
// Return an object that encapsulates this state.
return new UpdateFile(
(string) $data['publickey'],
Expand All @@ -87,7 +88,7 @@ protected function getVerificationKeysHttp($provider, $purpose = null)
);

// Decode the response body as a JSON object.
/** @var array{publickey: string, limited: bool, purpose: ?string, ledgerhash: string, metadata:array}[] $decoded */
/** @var array{publickey: string, limited: bool, purpose: ?string, ledgerhash: string, metadata:array}[] $decoded */
$decoded = json_decode($response['body'], true);
if (empty($decoded)) {
throw new GossamerException('No update file available');
Expand Down Expand Up @@ -116,10 +117,11 @@ protected function getVerificationKeysHttp($provider, $purpose = null)
* @param string $provider
* @param string $package
* @param string $version
* @param ?string $artifact
* @return array{publickey: string, signature: string, metadata:array}
* @throws GossamerException
*/
protected function getUpdateInfoHttp($provider, $package, $version)
protected function getUpdateInfoHttp($provider, $package, $version, $artifact = null)
{
// Fetch the HTTP response.
/** @var array{body: string} $response */
Expand All @@ -128,8 +130,16 @@ protected function getUpdateInfoHttp($provider, $package, $version)
);

// Decode the response body as a JSON object.
/** @var array<array-key, array{publickey: string, signature: string, metadata:array}> $decoded */
/** @var array<array-key, array{artifact: string, publickey: string, signature: string, metadata:array}> $decoded */
$decoded = json_decode($response['body'], true);
// If we have an artifact type, filter only the updates
if (!is_null($artifact) && !empty($decoded)) {
foreach ($decoded as $key => $row) {
if ($row['artifact'] !== $artifact) {
unset($decoded[$key]);
}
}
}
if (empty($decoded)) {
throw new GossamerException('No update file available');
}
Expand All @@ -143,9 +153,10 @@ protected function getUpdateInfoHttp($provider, $package, $version)
* @param string $provider
* @param string $package
* @param string $version
* @param ?string $artifact
* @return array{attestor: string, attestation: string, ledgerhash: string}[]
*/
protected function getAttestationsHttp($provider, $package, $version)
protected function getAttestationsHttp($provider, $package, $version, $artifact = null)
{
/** @var array{body: string} $response */
$response = $this->http->get(
Expand All @@ -154,9 +165,17 @@ protected function getAttestationsHttp($provider, $package, $version)

// Decode the response body as a JSON object.
/**
* @var array{attestor: string, attestation: string, ledgerhash: string}[] $decoded
* @var array{artifact: string, attestor: string, attestation: string, ledgerhash: string}[] $decoded
*/
$decoded = json_decode($response['body'], true);
// If we have an artifact type, filter only the updates
if (!is_null($artifact) && !empty($decoded)) {
foreach ($decoded as $key => $row) {
if ($row['artifact'] !== $artifact) {
unset($decoded[$key]);
}
}
}
if (empty($decoded)) {
return array();
}
Expand Down
7 changes: 4 additions & 3 deletions lib/Client/TrustMode/LocalTrust.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,16 @@ public function getVerificationKeys($provider, $purpose = null)
* @param string $provider
* @param string $package
* @param string $version
* @param ?string $artifact
* @return UpdateFile
*/
public function getUpdateInfo($provider, $package, $version)
public function getUpdateInfo($provider, $package, $version, $artifact = null)
{
/** @var array{publickey: string, signature: string, metadata: array} $data */
$data = $this->db->getRelease($provider, $package, $version);
$data = $this->db->getRelease($provider, $package, $version, $artifact);

/** @var array{attestor: string, attestation: string, ledgerhash: string}[] $attestations */
$attestations = $this->db->getAttestations($provider, $package, $version);
$attestations = $this->db->getAttestations($provider, $package, $version, $artifact);

return new UpdateFile(
(string) $data['publickey'],
Expand Down
3 changes: 2 additions & 1 deletion lib/Client/TrustModeInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ public function getVerificationKeys($provider, $purpose = null);
* @param string $provider
* @param string $package
* @param string $version
* @param ?string $artifact
* @return UpdateFile
*/
public function getUpdateInfo($provider, $package, $version);
public function getUpdateInfo($provider, $package, $version, $artifact = null);
}
54 changes: 39 additions & 15 deletions lib/Db/PDO.php
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ public function revokeKey($provider, $publicKey, array $meta = array(), $hash =
* @param string $provider
* @param string $package
* @param string $release
* @param ?string $artifact
* @param string $attestor
* @param string $attestation
* @param array $meta
Expand All @@ -172,14 +173,15 @@ public function attestUpdate(
$provider,
$package,
$release,
$artifact,
$attestor,
$attestation,
array $meta = array(),
$hash = ''
) {
$this->db->beginTransaction();
$attestorId = $this->getProviderId($attestor);
$releaseId = $this->getRelease($provider, $package, $release);
$releaseId = $this->getRelease($provider, $package, $release, $artifact);
/** @var array<string, scalar> $inserts */
$inserts = array(
'release_id' => $releaseId,
Expand All @@ -201,6 +203,7 @@ public function attestUpdate(
* @param string $package
* @param string $publicKey
* @param string $release
* @param ?string $artifact
* @param string $signature
* @param array $meta
* @param string $hash
Expand All @@ -212,6 +215,7 @@ public function appendUpdate(
$package,
$publicKey,
$release,
$artifact,
$signature,
array $meta = array(),
$hash = ''
Expand All @@ -223,6 +227,7 @@ public function appendUpdate(
$inserts = [
'package' => $packageId,
'version' => $release,
'artifact' => $artifact,
'publickey' => $publicKeyId,
'signature' => $signature,
'metadata' => json_encode($meta)
Expand All @@ -247,6 +252,7 @@ public function appendUpdate(
* @param string $package
* @param string $publicKey
* @param string $release
* @param ?string $artifact
* @param array $meta
* @param string $hash
* @return bool
Expand All @@ -257,6 +263,7 @@ public function revokeUpdate(
$package,
$publicKey,
$release,
$artifact = null,
array $meta = array(),
$hash = ''
) {
Expand All @@ -267,15 +274,19 @@ public function revokeUpdate(
if (!empty($hash)) {
$updates['revokehash'] = $hash;
}
$clause = [
'version' => $release,
'package' => $packageId
];
if (!is_null($artifact)) {
$clause['artifact'] = $artifact;
}

$this->db->beginTransaction();
$this->db->update(
self::TABLE_PACKAGE_RELEASES,
$updates,
[
'version' => $release,
'package' => $packageId
]
$clause
);
$this->updateMeta($hash);
return $this->db->commit();
Expand Down Expand Up @@ -460,28 +471,35 @@ public function isKeyLimited($providerName, $publicKey)
* @param string $providerName
* @param string $packageName
* @param string $version
* @param ?string $artifact
* @param int $offset
* @return array
* @throws \TypeError
* @throws GossamerException
*/
public function getRelease($providerName, $packageName, $version, $offset = 0)
public function getRelease($providerName, $packageName, $version, $artifact = null, $offset = 0)
{
if (!is_int($offset)) {
throw new \TypeError('Offset must be an integer');
}
/** @var array<string, int|string|bool|array|null> $results */
$results = $this->db->row(
"SELECT r.*
$queryString = "SELECT r.*
FROM " . self::TABLE_PACKAGE_RELEASES . " r
JOIN " . self::TABLE_PACKAGES . " p ON r.package = p.id
JOIN " . self::TABLE_PROVIDERS ." v ON p.provider = v.id
WHERE v.name = ? AND p.name = ? AND r.version = ?
OFFSET $offset LIMIT 1",
OFFSET $offset LIMIT 1";
$queryParams = array(
$providerName,
$packageName,
$version
);
if (!is_null($artifact)) {
$queryString .= " AND r.artifact = ?";
$queryParams []= $artifact;
}

/** @var array<string, int|string|bool|array|null> $results */
$results = $this->db->row($queryString, ...$queryParams);
if (empty($results)) {
throw new GossamerException("Version {$version} not found for package {$providerName}/{$packageName}");
}
Expand All @@ -495,21 +513,27 @@ public function getRelease($providerName, $packageName, $version, $offset = 0)
* @param string $providerName
* @param string $packageName
* @param string $version
* @param ?string $artifact
* @return array<array-key, array{attestor: string, attestation: string, ledgerhash: string}>
*/
public function getAttestations($providerName, $packageName, $version)
public function getAttestations($providerName, $packageName, $version, $artifact = null)
{
/** @var int $releaseId */
$releaseId = $this->db->cell(
"SELECT r.id
$queryString = "SELECT r.id
FROM " . self::TABLE_PACKAGE_RELEASES . " r
JOIN " . self::TABLE_PACKAGES . " p ON r.package = p.id
JOIN " . self::TABLE_PROVIDERS ." v ON p.provider = v.id
WHERE v.name = ? AND p.name = ? AND r.version = ? AND NOT r.revoked",
WHERE v.name = ? AND p.name = ? AND r.version = ? AND NOT r.revoked";
$queryParams = array(
$providerName,
$packageName,
$version
);
if (!is_null($artifact)) {
$queryString .= " AND r.artifact = ?";
$queryParams []= $artifact;
}
/** @var int $releaseId */
$releaseId = $this->db->cell($queryString, ...$queryParams);
if (empty($releaseId)) {
throw new GossamerException('Release not found');
}
Expand Down
Loading

0 comments on commit 7132082

Please sign in to comment.