Merge pull request #2653 from coreos-inc/new-signing-ui

Implement updated UI for displaying the signing status of a tag, now …
This commit is contained in:
josephschorr 2017-05-24 11:31:52 -04:00 committed by GitHub
commit 8e8470890a
12 changed files with 433 additions and 128 deletions

View file

@ -27,9 +27,4 @@ class RepositorySignatures(RepositoryParamResource):
if repo is None or not repo.trust_enabled: if repo is None or not repo.trust_enabled:
raise NotFound() raise NotFound()
tag_data, expiration = tuf_metadata_api.get_default_tags_with_expiration(namespace, repository) return {'delegations': tuf_metadata_api.get_all_tags_with_expiration(namespace, repository)}
return {
'tags': tag_data,
'expiration': expiration
}

View file

@ -8,37 +8,47 @@ from endpoints.api.signing import RepositorySignatures
from test.fixtures import * from test.fixtures import *
VALID_TARGETS = { VALID_TARGETS_MAP = {
'latest': { "targets/ci": {
'hashes': { "targets": {
'sha256': 'mLmxwTyUrqIRDaz8uaBapfrp3GPERfsDg2kiMujlteo=' "latest": {
"hashes": {
"sha256": "2Q8GLEgX62VBWeL76axFuDj/Z1dd6Zhx0ZDM6kNwPkQ="
}, },
'length': 1500 "length": 2111
},
'test_tag': {
'hashes': {
'sha256': '1234123'
},
'length': 50
} }
} },
"expiration": "2020-05-22T10:26:46.618176424-04:00"
},
"targets": {
"targets": {
"latest": {
"hashes": {
"sha256": "2Q8GLEgX62VBWeL76axFuDj/Z1dd6Zhx0ZDM6kNwPkQ="
},
"length": 2111
}
},
"expiration": "2020-05-22T10:26:01.953414888-04:00"}
}
def tags_equal(expected, actual): def tags_equal(expected, actual):
expected_tags = expected.get('tags') expected_tags = expected.get('delegations')
actual_tags = actual.get('tags') actual_tags = actual.get('delegations')
if expected_tags and actual_tags: if expected_tags and actual_tags:
return Counter(expected_tags) == Counter(actual_tags) return Counter(expected_tags) == Counter(actual_tags)
return expected == actual return expected == actual
@pytest.mark.parametrize('targets,expected', [ @pytest.mark.parametrize('targets_map,expected', [
(VALID_TARGETS, {'tags': VALID_TARGETS, 'expiration': 'expires'}), (VALID_TARGETS_MAP, {'delegations': VALID_TARGETS_MAP}),
({'bad': 'tags'}, {'tags': {'bad': 'tags'}, 'expiration': 'expires'}), ({'bad': 'tags'}, {'delegations': {'bad': 'tags'}}),
({}, {'tags': {}, 'expiration': 'expires'}), ({}, {'delegations': {}}),
(None, {'tags': None, 'expiration': 'expires'}), # API returns None on exceptions (None, {'delegations': None}), # API returns None on exceptions
]) ])
def test_get_signatures(targets, expected, client): def test_get_signatures(targets_map, expected, client):
with patch('endpoints.api.signing.tuf_metadata_api') as mock_tuf: with patch('endpoints.api.signing.tuf_metadata_api') as mock_tuf:
mock_tuf.get_default_tags_with_expiration.return_value = (targets, 'expires') mock_tuf.get_all_tags_with_expiration.return_value = targets_map
with client_with_identity('devtable', client) as cl: with client_with_identity('devtable', client) as cl:
params = {'repository': 'devtable/trusted'} params = {'repository': 'devtable/trusted'}
assert tags_equal(expected, conduct_api_call(cl, RepositorySignatures, 'GET', params, None, 200).json) assert tags_equal(expected, conduct_api_call(cl, RepositorySignatures, 'GET', params, None, 200).json)

View file

@ -43,7 +43,7 @@
.repo-panel-tags-element .image-track-line.start { .repo-panel-tags-element .image-track-line.start {
top: 18px; top: 18px;
height: 25px; height: 28px;
display: block; display: block;
} }
@ -144,6 +144,10 @@
padding-top: 0px; padding-top: 0px;
} }
.repo-panel-tags-element .signing-delegations-list {
margin-top: 8px;
}
@media (max-width: 1000px) { @media (max-width: 1000px) {
.repo-panel-tags-element .image-track { .repo-panel-tags-element .image-track {
display: none; display: none;

View file

@ -1,5 +1,17 @@
.manifest-label-list-element { .manifest-label-list-element {
padding-left: 6px; padding-left: 6px;
display: inline-block;
position: relative;
}
.manifest-label-list-element:before {
content: "\f02c";
font-family: FontAwesome;
position: absolute;
left: -22px;
top: 0px;
font-size: 15px;
color: #888;
} }
.manifest-label-list-element .none { .manifest-label-list-element .none {

View file

@ -2,10 +2,11 @@
text-align: center; text-align: center;
display: inline-block; display: inline-block;
cursor: default; cursor: default;
position: relative;
} }
.tag-signing-display-element .fa { .tag-signing-display-element .fa {
font-size: 18px; font-size: 24px;
} }
.tag-signing-display-element .fa.fa-question-circle { .tag-signing-display-element .fa.fa-question-circle {
@ -22,34 +23,153 @@
color: #9B9B9B; color: #9B9B9B;
} }
.tag-signing-display-element .signing-valid .okay, .tag-signing-display-element .signing-valid.okay-release {
.tag-signing-display-element .signing-valid .expires-soon {
color: #2FC98E; color: #2FC98E;
} }
.tag-signing-display-element .signing-valid.okay {
.tag-signing-display-element .signing-valid .expires-soon { color: #5f9dd0;
position: relative;
} }
.tag-signing-display-element .signing-valid .expires-soon:after { .tag-signing-display-element .signing-valid.partial-okay {
border-radius: 50%;
width: 6px;
height: 6px;
position: absolute;
bottom: 0px;
right: 0px;
z-index: 1;
display: inline-block;
content: " ";
background-color: #FCA657;
}
.tag-signing-display-element .signing-valid .expired {
color: #FCA657; color: #FCA657;
} }
.tag-signing-display-element .signing-invalid { .tag-signing-display-element .signing-invalid {
color: #D64456; color: #D64456;
} }
.tag-signing-display-element .indicator {
position: relative;
}
.tag-signing-display-element .expiring-soon {
border-radius: 100%;
background-color: #fbab62;
position: absolute;
right: 0px;
bottom: 0px;
width: 8px;
height: 8px;
z-index: 2;
}
.tag-signing-display-element .expired {
border-radius: 100%;
background-color: #ec5266;
position: absolute;
right: 0px;
bottom: 0px;
width: 8px;
height: 8px;
z-index: 3;
}
.tag-signing-display-element .invalid {
border-radius: 100%;
background-color: #ec5266;
position: absolute;
right: 0px;
bottom: 0px;
width: 8px;
height: 8px;
z-index: 4;
}
.tag-signing-display-element.extended {
display: block;
position: relative;
}
.tag-signing-display-element.extended .fa {
color: #888 !important;
font-size: 16px;
}
.tag-signing-display-element.extended .indicator {
font-size: 16px;
position: absolute;
left: -22px;
top: 4px;
}
.tag-signing-display-element.extended .delegations {
margin: 0px;
padding: 0px;
text-align: left;
padding-left: 6px;
padding-top: 6px;
}
.tag-signing-display-element.extended .delegations td {
padding: 4px;
border: 0px;
}
.tag-signing-display-element.extended .delegations .delegation {
padding: 4px;
background-color: #eee;
border-radius: 4px;
font-size: 13px;
padding-right: 6px;
display: inline-block;
}
.tag-signing-display-element.extended .delegations .delegation:before {
content: "\f00c";
font-family: FontAwesome;
margin-right: 2px;
margin-left: 2px;
display: inline-block;
font-size: 10px;
}
.tag-signing-display-element.extended .delegations .delegation.okay {
background-color: #d0deea;
}
.tag-signing-display-element.extended .delegations .delegation.okay:before {
color: #5f9dd0;
}
.tag-signing-display-element.extended .delegations .delegation.default {
background-color: #bdf1dd;
}
.tag-signing-display-element.extended .delegations .delegation.default:before {
color: #2FC98E;
}
.tag-signing-display-element.extended .delegations .delegation.warning {
background-color: #ffe0c4;
}
.tag-signing-display-element.extended .delegations .delegation.warning:before {
content: "\f12a";
color: #FCA657;
}
.tag-signing-display-element.extended .delegations .delegation.error {
background-color: #ffcad1;
}
.tag-signing-display-element.extended .delegations .delegation.error:before {
content: "\f00d";
color: #D64456;
}
.tag-signing-display-element.extended .delegations .delegation-name {
font-size: 14px;
}
.tag-signing-display-element.extended .delegations .delegation-info {
display: inline-block;
white-space: nowrap;
vertical-align: middle;
margin-left: 4px;
font-size: 12px;
}

View file

@ -131,7 +131,7 @@
<td class="signing-col hidden-xs" <td class="signing-col hidden-xs"
quay-require="['SIGNING']" quay-require="['SIGNING']"
ng-if="repository.trust_enabled"> ng-if="repository.trust_enabled">
<tag-signing-display tag="tag" signatures="repoSignatureInfo"></tag-signing-display> <tag-signing-display tag="tag" delegations="repoDelegationsInfo" compact="true"></tag-signing-display>
</td> </td>
<td class="hidden-xs"> <td class="hidden-xs">
<span bo-if="tag.last_modified" data-title="{{ tag.last_modified | amDateFormat:'dddd, MMMM Do YYYY, h:mm:ss a' }}" bs-tooltip> <span bo-if="tag.last_modified" data-title="{{ tag.last_modified | amDateFormat:'dddd, MMMM Do YYYY, h:mm:ss a' }}" bs-tooltip>
@ -261,9 +261,15 @@
</tr> </tr>
<tr ng-if="expandedView"> <tr ng-if="expandedView">
<td class="checkbox-col"></td> <td class="checkbox-col"></td>
<td class="labels-col" colspan="{{5 + (Features.SECURITY_SCANNER ? 1 : 0)}}"> <td class="labels-col" colspan="{{5 + (Features.SECURITY_SCANNER ? 1 : 0) + (repository.trust_enabled ? 1 : 0) }}">
<!-- Labels -->
<div class="manifest-label-list" repository="repository" <div class="manifest-label-list" repository="repository"
manifest-digest="tag.manifest_digest" cache="labelCache"></div> manifest-digest="tag.manifest_digest" cache="labelCache"></div>
<!-- Delegations -->
<div class="signing-delegations-list" ng-if="repository.trust_enabled">
<tag-signing-display compact="false" tag="tag" delegations="repoDelegationsInfo"></tag-signing-display>
</div>
</td> </td>
<td class="hidden-xs hidden-sm image-track" ng-repeat="it in imageTracks" <td class="hidden-xs hidden-sm image-track" ng-repeat="it in imageTracks"
ng-if="imageTracks.length <= maxTrackCount" bindonce> ng-if="imageTracks.length <= maxTrackCount" bindonce>

View file

@ -39,7 +39,7 @@ angular.module('quay').directive('repoPanelTags', function () {
$scope.labelCache = {}; $scope.labelCache = {};
$scope.imageVulnerabilities = {}; $scope.imageVulnerabilities = {};
$scope.repoSignatureInfo = null; $scope.repoDelegationsInfo = null;
$scope.defcon1 = {}; $scope.defcon1 = {};
$scope.hasDefcon1 = false; $scope.hasDefcon1 = false;
@ -50,16 +50,16 @@ angular.module('quay').directive('repoPanelTags', function () {
} }
$scope.repoSignatureError = false; $scope.repoSignatureError = false;
$scope.repoSignatureInfo = null; $scope.repoDelegationsInfo = null;
var params = { var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name 'repository': $scope.repository.namespace + '/' + $scope.repository.name
}; };
ApiService.getRepoSignatures(null, params).then(function(resp) { ApiService.getRepoSignatures(null, params).then(function(resp) {
$scope.repoSignatureInfo = resp; $scope.repoDelegationsInfo = resp;
}, function() { }, function() {
$scope.repoSignatureInfo = {'error': true}; $scope.repoDelegationsInfo = {'error': true};
}); });
}; };

View file

@ -1,5 +1,5 @@
<span class="tag-signing-display-element"> <span class="tag-signing-display-element" ng-class="{'extended': !$ctrl.compact}">
<span ng-switch on="$ctrl.signingStatus($ctrl.tag, $ctrl.signatures)"> <span class="indicator" ng-switch on="$ctrl.signingStatus($ctrl.tag, $ctrl.delegations)">
<!-- Loading --> <!-- Loading -->
<span ng-switch-when="loading"> <span ng-switch-when="loading">
<span class="cor-loader-inline"></span> <span class="cor-loader-inline"></span>
@ -17,33 +17,81 @@
<i class="fa shield-icon ci-shield-none"></i> <i class="fa shield-icon ci-shield-none"></i>
</span> </span>
<!-- Signature Valid --> <!-- Releases + all other targets signed -->
<span class="signing-valid" ng-switch-when="valid-signature"> <span class="signing-valid okay-release" ng-switch-when="all-signed"
<span ng-switch on="$ctrl.expirationStatus($ctrl.tag, $ctrl.signatures)"> data-title="The tag has valid and matching signatures" bs-tooltip>
<!-- But expired --> <i class="fa shield-icon ci-shield-check-full"></i>
<span class="expired" ng-switch-when="expired"
data-title="This tag has a matching, but expired signature" bs-tooltip> <span ng-if="$ctrl.getSigningInfo($ctrl.tag, $ctrl.delegations).hasExpiringSoon" class="expiring-soon"></span>
<i class="fa shield-icon ci-shield-invalid-outline"></i> <span ng-if="$ctrl.getSigningInfo($ctrl.tag, $ctrl.delegations).hasExpired" class="expired"></span>
<span ng-if="$ctrl.getSigningInfo($ctrl.tag, $ctrl.delegations).hasInvalid" class="invalid"></span>
</span> </span>
<!-- Expires soon --> <!-- Releases target signed -->
<span class="expires-soon" ng-switch-when="expires-soon" <span class="signing-valid okay-release" ng-switch-when="release-signed"
data-title="This tag has a valid and matching signature, but it is expiring soon on {{ $ctrl.signatures.expiration }}" bs-tooltip> data-title="The tag has a valid and matching default signature" bs-tooltip>
<i class="fa shield-icon ci-shield-check-outline"></i>
<span ng-if="$ctrl.getSigningInfo($ctrl.tag, $ctrl.delegations).hasExpiringSoon" class="expiring-soon"></span>
<span ng-if="$ctrl.getSigningInfo($ctrl.tag, $ctrl.delegations).hasExpired" class="expired"></span>
<span ng-if="$ctrl.getSigningInfo($ctrl.tag, $ctrl.delegations).hasInvalid" class="invalid"></span>
</span>
<!-- Non-releases target signed -->
<span class="signing-valid okay" ng-switch-when="non-release-signed"
data-title="The tag has valid and matching non-default signatures" bs-tooltip>
<i class="fa shield-icon ci-shield-check-outline"></i>
<span ng-if="$ctrl.getSigningInfo($ctrl.tag, $ctrl.delegations).hasExpiringSoon" class="expiring-soon"></span>
<span ng-if="$ctrl.getSigningInfo($ctrl.tag, $ctrl.delegations).hasExpired" class="expired"></span>
<span ng-if="$ctrl.getSigningInfo($ctrl.tag, $ctrl.delegations).hasInvalid" class="invalid"></span>
</span>
<!-- Non-releases target signed -->
<span class="signing-valid partial-okay" ng-switch-when="one-valid-signed"
data-title="The tag has at least one valid and matching non-default signature, but some are invalid or have expired" bs-tooltip>
<i class="fa shield-icon ci-shield-check-outline"></i> <i class="fa shield-icon ci-shield-check-outline"></i>
</span> </span>
<!-- Okay --> <!-- All signatures are invalid -->
<span class="okay" ng-switch-when="okay" <span class="signing-invalid" ng-switch-when="invalid-signed"
data-title="This tag has a valid and matching signature" bs-tooltip> data-title="There are no valid or non-expired signatures for this tag" bs-tooltip>
<i class="fa shield-icon ci-shield-check-outline"></i>
</span>
</span>
</span>
<!-- Signature Invalid -->
<span class="signing-invalid" ng-switch-when="invalid-signature"
data-title="The signed digest for this tag does not match the one pushed.<br><br>Signed: {{ this.signedDigest.substr(0, 12) }}<br>Pushed: {{ this.pushedDigest.substr(0, 12) }}" data-html="true" bs-tooltip>
<i class="fa shield-icon ci-shield-invalid-outline"></i> <i class="fa shield-icon ci-shield-invalid-outline"></i>
</span> </span>
</span> </span>
<!-- Delegations -->
<div class="delegations" ng-if="!$ctrl.compact && !$ctrl.getSigningInfo($ctrl.tag, $ctrl.delegations).delegations.length">
This tag has not been signed
</div>
<table class="delegations" ng-if="!$ctrl.compact && $ctrl.getSigningInfo($ctrl.tag, $ctrl.delegations).delegations.length">
<tr ng-repeat="delegation in $ctrl.getSigningInfo($ctrl.tag, $ctrl.delegations).delegations">
<td>
<span class="delegation" ng-class="{'default': $ctrl.isDefaultDelegation(delegation.delegationName), 'okay': delegation.hasMatchingHash && !delegation.isExpired && !delegation.isExpiringSoon, 'warning': delegation.hasMatchingHash && delegation.isExpiringSoon, 'error': !delegation.hasMatchingHash || delegation.isExpired}">
<span class="delegation-name" ng-if="delegation.delegationName == 'targets'">(Default)</span>
<span class="delegation-name" ng-if="delegation.delegationName != 'targets'">{{ delegation.delegationName.substr('targets/'.length) }}</span>
</span>
<div class="delegation-info visible-xs" ng-if="!delegation.hasMatchingHash || delegation.isExpiringSoon || delegation.isExpired">
<span class="failure-reason" ng-if="delegation.hasMatchingHash && delegation.isExpiringSoon">
Expiring soon: {{ delegation.expiration.format("YYYY-MM-DD HH:mm:ss") }}
</span>
<span class="failure-reason" ng-if="delegation.hasMatchingHash && delegation.isExpired">Signature expired</span>
<span class="failure-reason" ng-if="!delegation.hasMatchingHash">
Signature has different hash: {{ delegation.delegationHash.substr(0, 12) }}
</span>
</div>
</td>
<td class="hidden-xs">
<span class="delegation-info" ng-if="!delegation.hasMatchingHash || delegation.isExpiringSoon || delegation.isExpired">
<span class="failure-reason" ng-if="delegation.hasMatchingHash && delegation.isExpiringSoon">
Expiring soon: {{ delegation.expiration.format("YYYY-MM-DD HH:mm:ss") }}
</span>
<span class="failure-reason" ng-if="delegation.hasMatchingHash && delegation.isExpired">Signature expired</span>
<span class="failure-reason" ng-if="!delegation.hasMatchingHash">
Signature has different hash: {{ delegation.delegationHash.substr(0, 12) }}
</span>
</span>
</td>
</tr>
</table>
</span> </span>

View file

@ -1,7 +1,27 @@
import { Input, Component, Inject } from 'ng-metadata/core'; import { Input, Component, Inject } from 'ng-metadata/core';
import { ApostilleSignatureDocument, ApostilleTagDocument } from '../../../types/common.types'; import { ApostilleDelegationsSet, ApostilleSignatureDocument, ApostilleTagDocument } from '../../../types/common.types';
import * as moment from "moment"; import * as moment from "moment";
type TagSigningInfo = {
delegations: DelegationInfo[];
delegationsByName: {[delegationName: string]: DelegationInfo};
hasExpiringSoon: boolean;
hasExpired: boolean;
hasInvalid: boolean;
}
type DelegationInfo = {
delegationName: string;
delegationHash: string;
expiration: moment.Moment;
hasMatchingHash: boolean;
isExpired: boolean;
isExpiringSoon: boolean
};
var RELEASES = ['targets/releases', 'targets'];
/** /**
* A component that displays the signing status of a tag in the repository view. * A component that displays the signing status of a tag in the repository view.
*/ */
@ -10,17 +30,22 @@ import * as moment from "moment";
templateUrl: '/static/js/directives/ui/tag-signing-display/tag-signing-display.component.html', templateUrl: '/static/js/directives/ui/tag-signing-display/tag-signing-display.component.html',
}) })
export class TagSigningDisplayComponent { export class TagSigningDisplayComponent {
@Input('<') public compact: boolean;
@Input('<') public tag: any; @Input('<') public tag: any;
@Input('<') public signatures: ApostilleSignatureDocument; @Input('<') public delegations: ApostilleDelegationsSet;
private signedDigest: string; private cachedSigningInfo: TagSigningInfo | null = null;
private pushedDigest: string;
constructor(@Inject("$sanitize") private $sanitize: ng.sanitize.ISanitizeService) {} constructor(@Inject("$sanitize") private $sanitize: ng.sanitize.ISanitizeService) {}
private base64ToHex(base64String: string): string { private base64ToHex(base64String: string): string {
// Based on: http://stackoverflow.com/questions/39460182/decode-base64-to-hexadecimal-string-with-javascript // Based on: http://stackoverflow.com/questions/39460182/decode-base64-to-hexadecimal-string-with-javascript
try {
var raw = atob(base64String); var raw = atob(base64String);
} catch (e) {
return '(invalid)';
}
var hexString = ''; var hexString = '';
for (var i = 0; i < raw.length; ++i) { for (var i = 0; i < raw.length; ++i) {
var char = raw.charCodeAt(i); var char = raw.charCodeAt(i);
@ -30,49 +55,122 @@ export class TagSigningDisplayComponent {
return hexString; return hexString;
} }
private expirationStatus(tag: any, signatures: ApostilleSignatureDocument): string { private buildDelegationInfo(tag: any, delegationName: string, delegation: ApostilleSignatureDocument): DelegationInfo {
if (!signatures || !signatures.expiration) { var digest_without_prefix = tag.manifest_digest.substr('sha256:'.length);
return 'unknown'; var hex_signature = this.base64ToHex(delegation.targets[tag.name].hashes['sha256']);
}
var expires = moment(signatures.expiration); var expires = moment(delegation.expiration);
var now = moment(); var now = moment();
if (expires.isSameOrBefore(now)) {
return 'expired';
}
var withOneWeek = moment().add('1', 'w'); var withOneWeek = moment().add('1', 'w');
if (expires.isSameOrBefore(withOneWeek)) {
return 'expires-soon'; return {
'delegationName': delegationName,
'hasMatchingHash': digest_without_prefix == hex_signature,
'expiration': expires,
'delegationHash': hex_signature,
'isExpired': expires.isSameOrBefore(now),
'isExpiringSoon': !expires.isSameOrBefore(now) && expires.isSameOrBefore(withOneWeek),
}
} }
return 'okay'; private buildTagSigningInfo(tag: any, delegationSet: ApostilleDelegationsSet): TagSigningInfo {
var info = {
'delegations': [],
'delegationsByName': {},
'hasExpired': false,
'hasExpiringSoon': false,
'hasInvalid': false,
} }
private signingStatus(tag: any, signatures: ApostilleSignatureDocument): string { // Find all delegations containing the tag as a target.
if (!tag || !signatures) { Object.keys(delegationSet.delegations).forEach((delegationName) => {
var delegation = delegationSet.delegations[delegationName];
if (delegation.targets[tag.name]) {
var DelegationInfo = this.buildDelegationInfo(tag, delegationName, delegation)
info.delegations.push(DelegationInfo);
info.delegationsByName[delegationName] = DelegationInfo;
if (DelegationInfo.isExpired) {
info.hasExpired = true;
}
if (DelegationInfo.isExpiringSoon) {
info.hasExpiringSoon = true;
}
if (!DelegationInfo.hasMatchingHash) {
info.hasInvalid = true;
}
}
});
return info;
}
private isDefaultDelegation(name: string): boolean {
return RELEASES.indexOf(name) >= 0;
}
private getSigningInfo(tag: any, delegationSet: ApostilleDelegationsSet): TagSigningInfo {
if (!this.cachedSigningInfo) {
this.cachedSigningInfo = this.buildTagSigningInfo(tag, delegationSet);
}
return this.cachedSigningInfo;
}
private signingStatus(tag: any, delegationSet: ApostilleDelegationsSet): string {
if (!tag || !delegationSet) {
return 'loading'; return 'loading';
} }
if (signatures.error || !signatures.tags) { if (!tag.manifest_digest) {
return 'error';
}
var tag_info = signatures.tags[tag.name];
if (!tag_info || !tag.manifest_digest) {
return 'not-signed'; return 'not-signed';
} }
var digest_without_prefix = tag.manifest_digest.substr('sha256:'.length); if (delegationSet.error) {
var hex_signature = this.base64ToHex(tag_info.hashes['sha256']); return 'error';
if (hex_signature == digest_without_prefix) {
return 'valid-signature';
} else {
this.signedDigest = this.$sanitize(hex_signature);
this.pushedDigest = this.$sanitize(digest_without_prefix);
return 'invaid-signature';
} }
// Check if signed at all.
var signingInfo = this.getSigningInfo(tag, delegationSet);
if (!signingInfo.delegations.length) {
return 'not-signed';
}
// Check if all delegations are signed and valid.
var allReleasesValid = true;
var oneReleaseValid = false;
this.cachedSigningInfo.delegations.forEach(function(delegation) {
var isValid = delegation.hasMatchingHash && !delegation.isExpired;
if (isValid) {
oneReleaseValid = true;
}
allReleasesValid = allReleasesValid && isValid;
});
// Check if the special RELEASES target(s) is/are signed and valid.
var releasesDelegation = null;
RELEASES.forEach((releaseTarget) => {
var delegation = this.cachedSigningInfo.delegationsByName[releaseTarget];
if (delegation && !releasesDelegation) {
releasesDelegation = delegation;
}
});
if (releasesDelegation && releasesDelegation.hasMatchingHash && !releasesDelegation.isExpired) {
if (allReleasesValid && this.cachedSigningInfo.delegations.length > 1) {
return 'all-signed';
} else {
return 'release-signed';
}
}
if (allReleasesValid || oneReleaseValid) {
return 'non-release-signed';
}
return 'invalid-signed';
} }
} }

View file

@ -122,6 +122,16 @@ export type TriggerConfig = {
}; };
/**
* Represents a set of apostille delegations.
*/
export type ApostilleDelegationsSet = {
delegations: {[delegationName: string]: ApostilleSignatureDocument};
// The error that occurred, if any.
error: string | null;
};
/** /**
* Represents an apostille signature document, with extra expiration information. * Represents an apostille signature document, with extra expiration information.
*/ */
@ -130,10 +140,7 @@ export type ApostilleSignatureDocument = {
expiration: string expiration: string
// Object of information for each tag. // Object of information for each tag.
tags: {string: ApostilleTagDocument} targets: {string: ApostilleTagDocument}
// If true, an error occurred while trying to load this document.
error: boolean
}; };

View file

@ -11,7 +11,7 @@ from data.database import CloseForLongOperation
from util.abchelpers import nooper from util.abchelpers import nooper
from util.failover import failover, FailoverException from util.failover import failover, FailoverException
from util.security.instancekeys import InstanceKeys from util.security.instancekeys import InstanceKeys
from util.security.registry_jwt import build_context_and_subject, generate_bearer_token, QUAY_TUF_ROOT from util.security.registry_jwt import build_context_and_subject, generate_bearer_token, QUAY_TUF_ROOT, SIGNER_TUF_ROOT
DEFAULT_HTTP_HEADERS = {'Connection': 'close'} DEFAULT_HTTP_HEADERS = {'Connection': 'close'}
@ -151,15 +151,20 @@ class ImplementedTUFMetadataAPI(TUFMetadataAPIInterface):
if not targets_file: if not targets_file:
targets_file = 'targets.json' targets_file = 'targets.json'
targets_name = targets_file
if targets_name.endswith('.json'):
targets_name = targets_name[:-5]
if not targets_map: if not targets_map:
targets_map = {} targets_map = {}
signed = self._get_signed(namespace, repository, targets_file) signed = self._get_signed(namespace, repository, targets_file)
if not signed: if not signed:
return None targets_map[targets_name] = None
return targets_map
if signed.get('targets'): if signed.get('targets'):
targets_map[targets_file] = { targets_map[targets_name] = {
'targets': signed.get('targets'), 'targets': signed.get('targets'),
'expiration': signed.get('expires'), 'expiration': signed.get('expires'),
} }
@ -167,7 +172,7 @@ class ImplementedTUFMetadataAPI(TUFMetadataAPIInterface):
delegation_names = [role.get('name') for role in signed.get('delegations').get('roles')] delegation_names = [role.get('name') for role in signed.get('delegations').get('roles')]
for delegation in delegation_names: for delegation in delegation_names:
targets_map = self.get_all_tags_with_expiration(namespace, repository, targets_file=delegation, targets_map=targets_map) targets_map = self.get_all_tags_with_expiration(namespace, repository, targets_file=delegation + '.json', targets_map=targets_map)
return targets_map return targets_map
@ -235,7 +240,7 @@ class ImplementedTUFMetadataAPI(TUFMetadataAPIInterface):
'name': gun, 'name': gun,
'actions': actions, 'actions': actions,
}] }]
context, subject = build_context_and_subject(user=None, token=None, oauthtoken=None, tuf_root=QUAY_TUF_ROOT) context, subject = build_context_and_subject(user=None, token=None, oauthtoken=None, tuf_root=SIGNER_TUF_ROOT)
token = generate_bearer_token(self._config["SERVER_HOSTNAME"], subject, context, access, token = generate_bearer_token(self._config["SERVER_HOSTNAME"], subject, context, access,
TOKEN_VALIDITY_LIFETIME_S, self._instance_keys) TOKEN_VALIDITY_LIFETIME_S, self._instance_keys)
return {'Authorization': 'Bearer %s' % token} return {'Authorization': 'Bearer %s' % token}

View file

@ -177,7 +177,7 @@ def test_get_default_tags(response_code, response_body, expected):
(200, valid_targets_with_delegation, valid_delegation, { (200, valid_targets_with_delegation, valid_delegation, {
'targets/devs': { 'targets': valid_delegation['signed']['targets'], 'targets/devs': { 'targets': valid_delegation['signed']['targets'],
'expiration': valid_delegation['signed']['expires']}}), 'expiration': valid_delegation['signed']['expires']}}),
(200, {'garbage': 'data'}, {'garbage': 'data'}, None) (200, {'garbage': 'data'}, {'garbage': 'data'}, {'targets': None})
]) ])
def test_get_all_tags(response_code, response_body1, response_body2, expected): def test_get_all_tags(response_code, response_body1, response_body2, expected):
app = Flask(__name__) app = Flask(__name__)