From c33ed8f597ca1453822455aee4b3c5f0527ed946 Mon Sep 17 00:00:00 2001
From: Joseph Schorr <josephschorr@users.noreply.github.com>
Date: Tue, 16 May 2017 17:07:09 -0400
Subject: [PATCH] Implement updated UI for displaying the signing status of a
 tag, now that we support multiple delegations

The icon now represents the status of the multiple delegations, and we show each delegation in the "Expanded" view.
---
 endpoints/api/signing.py                      |   7 +-
 .../directives/repo-view/repo-panel-tags.css  |   6 +-
 .../css/directives/ui/manifest-label-list.css |  12 ++
 .../css/directives/ui/tag-signing-display.css | 156 +++++++++++++++---
 .../directives/repo-view/repo-panel-tags.html |  10 +-
 .../directives/repo-view/repo-panel-tags.js   |   8 +-
 .../tag-signing-display.component.html        |  97 ++++++++---
 .../tag-signing-display.component.ts          | 156 ++++++++++++++----
 static/js/types/common.types.ts               |  15 +-
 9 files changed, 368 insertions(+), 99 deletions(-)

diff --git a/endpoints/api/signing.py b/endpoints/api/signing.py
index 2758a999d..aa426ff7c 100644
--- a/endpoints/api/signing.py
+++ b/endpoints/api/signing.py
@@ -27,9 +27,4 @@ class RepositorySignatures(RepositoryParamResource):
     if repo is None or not repo.trust_enabled:
       raise NotFound()
 
-    tag_data, expiration = tuf_metadata_api.get_default_tags_with_expiration(namespace, repository)
-    return {
-      'tags': tag_data,
-      'expiration': expiration
-    }
-
+    return {'delegations': tuf_metadata_api.get_all_tags_with_expiration(namespace, repository)}
diff --git a/static/css/directives/repo-view/repo-panel-tags.css b/static/css/directives/repo-view/repo-panel-tags.css
index 7e03596bf..c5ca2543a 100644
--- a/static/css/directives/repo-view/repo-panel-tags.css
+++ b/static/css/directives/repo-view/repo-panel-tags.css
@@ -43,7 +43,7 @@
 
 .repo-panel-tags-element .image-track-line.start {
   top: 18px;
-  height: 25px;
+  height: 28px;
   display: block;
 }
 
@@ -144,6 +144,10 @@
   padding-top: 0px;
 }
 
+.repo-panel-tags-element .signing-delegations-list {
+  margin-top: 8px;
+}
+
 @media (max-width: 1000px) {
   .repo-panel-tags-element .image-track {
     display: none;
diff --git a/static/css/directives/ui/manifest-label-list.css b/static/css/directives/ui/manifest-label-list.css
index 840f1b737..8cf247d42 100644
--- a/static/css/directives/ui/manifest-label-list.css
+++ b/static/css/directives/ui/manifest-label-list.css
@@ -1,5 +1,17 @@
 .manifest-label-list-element {
   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 {
diff --git a/static/css/directives/ui/tag-signing-display.css b/static/css/directives/ui/tag-signing-display.css
index d05d4608b..d51866977 100644
--- a/static/css/directives/ui/tag-signing-display.css
+++ b/static/css/directives/ui/tag-signing-display.css
@@ -2,10 +2,11 @@
     text-align: center;
     display: inline-block;
     cursor: default;
+    position: relative;
 }
 
 .tag-signing-display-element .fa {
-    font-size: 18px;
+    font-size: 24px;
 }
 
 .tag-signing-display-element .fa.fa-question-circle {
@@ -22,34 +23,145 @@
     color: #9B9B9B;
 }
 
-.tag-signing-display-element .signing-valid .okay,
-.tag-signing-display-element .signing-valid .expires-soon {
+.tag-signing-display-element .signing-valid.okay-release {
     color: #2FC98E;
 }
 
-
-.tag-signing-display-element .signing-valid .expires-soon {
-    position: relative;
+.tag-signing-display-element .signing-valid.okay {
+    color: #5f9dd0;
 }
 
-.tag-signing-display-element .signing-valid .expires-soon:after {
-    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 {
+.tag-signing-display-element .signing-valid.partial-okay {
     color: #FCA657;
 }
 
 .tag-signing-display-element .signing-invalid {
     color: #D64456;
-}
\ No newline at end of file
+}
+
+.tag-signing-display-element .indicator {
+    position: relative;
+}
+
+.tag-signing-display-element .expiring-soon {
+  border-radius: 100%;
+  background-color: #ffe0c4;
+
+  position: absolute;
+  right: 0px;
+  bottom: 0px;
+  width: 8px;
+  height: 8px;
+  z-index: 2;
+}
+
+.tag-signing-display-element .expired {
+  border-radius: 100%;
+  background-color: #ffcad1;
+
+  position: absolute;
+  right: 0px;
+  bottom: 0px;
+  width: 8px;
+  height: 8px;
+  z-index: 3;
+}
+
+.tag-signing-display-element .invalid {
+  border-radius: 100%;
+  background-color: #ffcad1;
+
+  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: #bdf1dd;
+}
+
+.tag-signing-display-element.extended .delegations .delegation.okay: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;
+}
+
diff --git a/static/directives/repo-view/repo-panel-tags.html b/static/directives/repo-view/repo-panel-tags.html
index 154dcfd97..a3b1d3e5a 100644
--- a/static/directives/repo-view/repo-panel-tags.html
+++ b/static/directives/repo-view/repo-panel-tags.html
@@ -131,7 +131,7 @@
       <td class="signing-col hidden-xs"
           quay-require="['SIGNING']"
           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 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>
@@ -261,9 +261,15 @@
     </tr>
     <tr ng-if="expandedView">
       <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"
              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 class="hidden-xs hidden-sm image-track" ng-repeat="it in imageTracks"
           ng-if="imageTracks.length <= maxTrackCount" bindonce>
diff --git a/static/js/directives/repo-view/repo-panel-tags.js b/static/js/directives/repo-view/repo-panel-tags.js
index bcd4c6a15..55d544575 100644
--- a/static/js/directives/repo-view/repo-panel-tags.js
+++ b/static/js/directives/repo-view/repo-panel-tags.js
@@ -39,7 +39,7 @@ angular.module('quay').directive('repoPanelTags', function () {
       $scope.labelCache = {};
 
       $scope.imageVulnerabilities = {};
-      $scope.repoSignatureInfo = null;
+      $scope.repoDelegationsInfo = null;
 
       $scope.defcon1 = {};
       $scope.hasDefcon1 = false;
@@ -50,16 +50,16 @@ angular.module('quay').directive('repoPanelTags', function () {
         }
 
         $scope.repoSignatureError = false;
-        $scope.repoSignatureInfo = null;
+        $scope.repoDelegationsInfo = null;
 
         var params = {
           'repository': $scope.repository.namespace + '/' + $scope.repository.name
         };
 
         ApiService.getRepoSignatures(null, params).then(function(resp) {
-          $scope.repoSignatureInfo = resp;
+          $scope.repoDelegationsInfo = resp;
         }, function() {
-          $scope.repoSignatureInfo = {'error': true};
+          $scope.repoDelegationsInfo = {'error': true};
         });
       };
 
diff --git a/static/js/directives/ui/tag-signing-display/tag-signing-display.component.html b/static/js/directives/ui/tag-signing-display/tag-signing-display.component.html
index be57ff502..a0d2c761e 100644
--- a/static/js/directives/ui/tag-signing-display/tag-signing-display.component.html
+++ b/static/js/directives/ui/tag-signing-display/tag-signing-display.component.html
@@ -1,5 +1,5 @@
-<span class="tag-signing-display-element">
- <span ng-switch on="$ctrl.signingStatus($ctrl.tag, $ctrl.signatures)">
+<span class="tag-signing-display-element" ng-class="{'extended': !$ctrl.compact}">
+ <span class="indicator" ng-switch on="$ctrl.signingStatus($ctrl.tag, $ctrl.delegations)">
   <!-- Loading -->
   <span ng-switch-when="loading">
     <span class="cor-loader-inline"></span>
@@ -17,33 +17,80 @@
     <i class="fa shield-icon ci-shield-none"></i>
   </span>
 
-  <!-- Signature Valid -->
-  <span class="signing-valid" ng-switch-when="valid-signature">
-    <span ng-switch on="$ctrl.expirationStatus($ctrl.tag, $ctrl.signatures)">
-      <!-- But expired -->
-      <span class="expired" ng-switch-when="expired"
-            data-title="This tag has a matching, but expired signature" bs-tooltip>
-        <i class="fa shield-icon ci-shield-invalid-outline"></i>
-      </span>
+  <!-- Releases + all other targets signed -->
+  <span class="signing-valid okay-release" ng-switch-when="all-signed"
+        data-title="The tag has valid and matching signatures" bs-tooltip>
+    <i class="fa shield-icon ci-shield-check-full"></i>
 
-      <!-- Expires soon -->
-      <span class="expires-soon" ng-switch-when="expires-soon"
-            data-title="This tag has a valid and matching signature, but it is expiring soon on {{ $ctrl.signatures.expiration }}" bs-tooltip>
-        <i class="fa shield-icon ci-shield-check-outline"></i>
-      </span>
-
-      <!-- Okay -->
-      <span class="okay" ng-switch-when="okay"
-            data-title="This tag has a valid and matching signature" bs-tooltip>
-        <i class="fa shield-icon ci-shield-check-outline"></i>
-      </span>
-    </span>
+    <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>
 
-  <!-- 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>
+  <!-- Releases target signed -->
+  <span class="signing-valid okay-release" ng-switch-when="release-signed"
+        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>
+  </span>
+
+  <!-- All signatures are invalid -->
+  <span class="signing-invalid" ng-switch-when="invalid-signed"
+        data-title="There are no valid or non-expired signatures for this tag" bs-tooltip>
     <i class="fa shield-icon ci-shield-invalid-outline"></i>
   </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="{'okay': delegation.hasMatchingHash && !delegation.isExpired && !delegation.isExpiringSoon, 'warning': delegation.hasMatchingHash && delegation.isExpiringSoon, 'error': !delegation.hasMatchingHash || delegation.isExpired}">
+         <span class="delegation-name">{{ 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>
\ No newline at end of file
diff --git a/static/js/directives/ui/tag-signing-display/tag-signing-display.component.ts b/static/js/directives/ui/tag-signing-display/tag-signing-display.component.ts
index 67a7bc02d..20bc1b41c 100644
--- a/static/js/directives/ui/tag-signing-display/tag-signing-display.component.ts
+++ b/static/js/directives/ui/tag-signing-display/tag-signing-display.component.ts
@@ -1,7 +1,27 @@
 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";
 
+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';
+
 /**
  * A component that displays the signing status of a tag in the repository view.
  */
@@ -10,11 +30,11 @@ import * as moment from "moment";
   templateUrl: '/static/js/directives/ui/tag-signing-display/tag-signing-display.component.html',
 })
 export class TagSigningDisplayComponent {
+  @Input('<') public compact: boolean;
   @Input('<') public tag: any;
-  @Input('<') public signatures: ApostilleSignatureDocument;
+  @Input('<') public delegations: ApostilleDelegationsSet;
 
-  private signedDigest: string;
-  private pushedDigest: string;
+  private cachedSigningInfo: tagSigningInfo | null = null;
 
   constructor(@Inject("$sanitize") private $sanitize: ng.sanitize.ISanitizeService) {}
 
@@ -30,49 +50,115 @@ export class TagSigningDisplayComponent {
     return hexString;
   }
 
-  private expirationStatus(tag: any, signatures: ApostilleSignatureDocument): string {
-    if (!signatures || !signatures.expiration) {
-      return 'unknown';
-    }
+  private buildDelegationInfo(tag: any, delegationName: string, delegation: ApostilleSignatureDocument): delegationInfo {
+    var digest_without_prefix = tag.manifest_digest.substr('sha256:'.length);
+    var hex_signature = this.base64ToHex(delegation.targets[tag.name].hashes['sha256']);
 
-    var expires = moment(signatures.expiration);
+    var expires = moment(delegation.expiration);
     var now = moment();
-
-    if (expires.isSameOrBefore(now)) {
-      return 'expired';
-    }
-
     var withOneWeek = moment().add('1', 'w');
-    if (expires.isSameOrBefore(withOneWeek)) {
-      return 'expires-soon';
-    }
 
-    return 'okay';
+    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),
+    }
   }
 
-  private signingStatus(tag: any, signatures: ApostilleSignatureDocument): string {
-    if (!tag || !signatures) {
+  private buildTagSigningInfo(tag: any, delegationSet: ApostilleDelegationsSet): tagSigningInfo {
+    var info = {
+      'delegations': [],
+      'delegationsByName': {},
+      'hasExpired': false,
+      'hasExpiringSoon': false,
+      'hasInvalid': false,
+    }
+
+    // Find all delegations containing the tag as a target.
+    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 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';
     }
 
-    if (signatures.error || !signatures.tags) {
-      return 'error';
-    }
-
-    var tag_info = signatures.tags[tag.name];
-    if (!tag_info || !tag.manifest_digest) {
+    if (!tag.manifest_digest) {
       return 'not-signed';
     }
 
-    var digest_without_prefix = tag.manifest_digest.substr('sha256:'.length);
-    var hex_signature = this.base64ToHex(tag_info.hashes['sha256']);
-
-    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';
+    if (delegationSet.error) {
+      return 'error';
     }
+
+    // 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 is signed and valid.
+    var releasesDelegation = this.cachedSigningInfo.delegationsByName[RELEASES];
+    if (releasesDelegation && releasesDelegation.hasMatchingHash && !releasesDelegation.isExpired) {
+      if (allReleasesValid && this.cachedSigningInfo.delegations.length > 1) {
+        return 'all-signed';
+      } else {
+        return 'release-signed';
+      }
+    }
+
+    if (allReleasesValid) {
+      return 'non-release-signed';
+    }
+
+    if (oneReleaseValid) {
+      return 'one-valid-signed';
+    }
+
+    return 'invalid-signed';
   }
 }
\ No newline at end of file
diff --git a/static/js/types/common.types.ts b/static/js/types/common.types.ts
index f3331cc71..c1cff8c90 100644
--- a/static/js/types/common.types.ts
+++ b/static/js/types/common.types.ts
@@ -105,6 +105,16 @@ export type Trigger = {
 };
 
 
+/**
+ * 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.
  */
@@ -113,10 +123,7 @@ export type ApostilleSignatureDocument = {
   expiration: string
 
   // Object of information for each tag.
-  tags: {string: ApostilleTagDocument}
-
-  // If true, an error occurred while trying to load this document.
-  error: boolean
+  targets: {string: ApostilleTagDocument}
 };