This is an automated email from the ASF dual-hosted git repository.
ocket8888 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git
The following commit(s) were added to refs/heads/master by this push:
new 730694b8ef TPv2 Add the ability to inspect a cert/DS (#7555)
730694b8ef is described below
commit 730694b8efd8b49350d72189b43e72ef96f96013
Author: Steve Hamrick <[email protected]>
AuthorDate: Mon Jul 31 16:31:19 2023 -0600
TPv2 Add the ability to inspect a cert/DS (#7555)
* Add SSL Cert viewer
* Better tests
* Fix build warning and add sidebar entry
* Licenses
* Update package-lock after merge
* Lint
* Lint
* Obscure private key
* Forgot file
* Style as textarea
* Code review fixes
* Fix date display
* Code review changes
* Use shorthand catch
* Fix github merge using wrong whitespace
---------
Co-authored-by: Steve Hamrick <[email protected]>
---
experimental/traffic-portal/angular.json | 3 +-
experimental/traffic-portal/package-lock.json | 24 ++-
experimental/traffic-portal/package.json | 2 +
.../src/app/api/delivery-service.service.spec.ts | 85 ++++++---
.../src/app/api/delivery-service.service.ts | 15 +-
.../app/api/testing/delivery-service.service.ts | 24 +++
.../traffic-portal/src/app/app.ui.module.ts | 2 +
.../certs/cert-author/cert-author.component.html | 41 +++++
.../certs/cert-author/cert-author.component.scss | 22 +++
.../cert-author/cert-author.component.spec.ts | 51 ++++++
.../certs/cert-author/cert-author.component.ts | 30 ++++
.../certs/cert-detail/cert-detail.component.html | 68 ++++++++
.../certs/cert-detail/cert-detail.component.scss | 34 ++++
.../cert-detail/cert-detail.component.spec.ts | 167 ++++++++++++++++++
.../certs/cert-detail/cert-detail.component.ts | 133 ++++++++++++++
.../certs/cert-viewer/cert-viewer.component.html | 77 ++++++++
.../certs/cert-viewer/cert-viewer.component.scss | 35 ++++
.../cert-viewer/cert-viewer.component.spec.ts | 161 +++++++++++++++++
.../certs/cert-viewer/cert-viewer.component.ts | 193 +++++++++++++++++++++
.../src/app/core/certs/cert.util.spec.ts | 55 ++++++
.../traffic-portal/src/app/core/certs/cert.util.ts | 51 ++++++
.../src/app/core/certs/certs.module.ts | 49 ++++++
.../traffic-portal/src/app/core/core.module.ts | 12 +-
.../statuses-table/statuses-table.component.html | 2 +-
.../statuses-table/statuses-table.component.ts | 6 -
.../app/shared/navigation/navigation.service.ts | 4 +
26 files changed, 1307 insertions(+), 39 deletions(-)
diff --git a/experimental/traffic-portal/angular.json
b/experimental/traffic-portal/angular.json
index 1ed4616576..5b2c52a685 100644
--- a/experimental/traffic-portal/angular.json
+++ b/experimental/traffic-portal/angular.json
@@ -21,7 +21,8 @@
"builder":
"@angular-builders/custom-webpack:browser",
"options": {
"allowedCommonJsDependencies": [
- "chart.js"
+ "chart.js",
+ "node-forge"
],
"customWebpackConfig": {
"path":
"src/compress-webpack.config.js"
diff --git a/experimental/traffic-portal/package-lock.json
b/experimental/traffic-portal/package-lock.json
index 4f9d959852..b03d8c3a76 100644
--- a/experimental/traffic-portal/package-lock.json
+++ b/experimental/traffic-portal/package-lock.json
@@ -27,6 +27,7 @@
"chart.js": "^2.9.4",
"compression-webpack-plugin": "^10.0.0",
"express": "^4.15.2",
+ "node-forge": "^1.3.1",
"rxjs": "~6.6.0",
"trafficops-types": "^4.0.10",
"tslib": "^2.0.0",
@@ -50,6 +51,7 @@
"@types/jasminewd2": "~2.0.3",
"@types/nightwatch": "^2.3.22",
"@types/node": "^16.18.11",
+ "@types/node-forge": "^1.3.2",
"@typescript-eslint/eslint-plugin": "^5.59.2",
"@typescript-eslint/parser": "^5.59.2",
"axios": "^0.27.2",
@@ -5460,6 +5462,15 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.28.tgz",
"integrity":
"sha512-SNMfiPqsiPoYfmyi+2qnDO4nZyMIOCab/CW+Slcml0lhIzkOizYzWtt/A7tgB3TSitd+YJKi8fSC2Cpm/VCp7A=="
},
+ "node_modules/@types/node-forge": {
+ "version": "1.3.2",
+ "resolved":
"https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.2.tgz",
+ "integrity":
"sha512-TzX3ahoi9xbmaoT58smrBu7oa6dQXb/+PTNCslZyD/55tlJ/osofIMClzZsoo6buDFrg7e4DvVGkZqVgv6OLxw==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/parse-json": {
"version": "4.0.0",
"resolved":
"https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
@@ -15083,7 +15094,6 @@
"version": "1.3.1",
"resolved":
"https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
"integrity":
"sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
- "dev": true,
"engines": {
"node": ">= 6.13.0"
}
@@ -24628,6 +24638,15 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.28.tgz",
"integrity":
"sha512-SNMfiPqsiPoYfmyi+2qnDO4nZyMIOCab/CW+Slcml0lhIzkOizYzWtt/A7tgB3TSitd+YJKi8fSC2Cpm/VCp7A=="
},
+ "@types/node-forge": {
+ "version": "1.3.2",
+ "resolved":
"https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.2.tgz",
+ "integrity":
"sha512-TzX3ahoi9xbmaoT58smrBu7oa6dQXb/+PTNCslZyD/55tlJ/osofIMClzZsoo6buDFrg7e4DvVGkZqVgv6OLxw==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
"@types/parse-json": {
"version": "4.0.0",
"resolved":
"https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
@@ -31971,8 +31990,7 @@
"node-forge": {
"version": "1.3.1",
"resolved":
"https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
- "integrity":
"sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
- "dev": true
+ "integrity":
"sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA=="
},
"node-gyp": {
"version": "9.3.1",
diff --git a/experimental/traffic-portal/package.json
b/experimental/traffic-portal/package.json
index b9d6f735a8..9dd956b754 100644
--- a/experimental/traffic-portal/package.json
+++ b/experimental/traffic-portal/package.json
@@ -67,6 +67,7 @@
"chart.js": "^2.9.4",
"compression-webpack-plugin": "^10.0.0",
"express": "^4.15.2",
+ "node-forge": "^1.3.1",
"rxjs": "~6.6.0",
"trafficops-types": "^4.0.10",
"tslib": "^2.0.0",
@@ -90,6 +91,7 @@
"@types/jasminewd2": "~2.0.3",
"@types/nightwatch": "^2.3.22",
"@types/node": "^16.18.11",
+ "@types/node-forge": "^1.3.2",
"@typescript-eslint/eslint-plugin": "^5.59.2",
"@typescript-eslint/parser": "^5.59.2",
"axios": "^0.27.2",
diff --git
a/experimental/traffic-portal/src/app/api/delivery-service.service.spec.ts
b/experimental/traffic-portal/src/app/api/delivery-service.service.spec.ts
index 6e304681a3..64e96b42dd 100644
--- a/experimental/traffic-portal/src/app/api/delivery-service.service.spec.ts
+++ b/experimental/traffic-portal/src/app/api/delivery-service.service.spec.ts
@@ -14,7 +14,7 @@
*/
import { HttpClientTestingModule, HttpTestingController } from
"@angular/common/http/testing";
import { TestBed } from "@angular/core/testing";
-import { DSStats, DSStatsMetricType } from "trafficops-types";
+import { DSStats, DSStatsMetricType, ResponseDeliveryServiceSSLKey } from
"trafficops-types";
import { constructDataSetFromResponse, DeliveryServiceService } from
"./delivery-service.service";
@@ -96,6 +96,15 @@ export const testDS = {
xmlId: "testquest",
};
+/** A dummy DS SSL Key for testing */
+export const testDSSSLKeys: ResponseDeliveryServiceSSLKey = {
+ cdn: testDS.cdnName,
+ certificate: {crt: "", csr: "", key: ""},
+ deliveryservice: testDS.xmlId,
+ expiration: new Date(),
+ version: ""
+};
+
/**
* Generates a basic set of DSStats data for use in tests.
*
@@ -369,7 +378,7 @@ describe("DeliveryServiceService", () => {
expect(req.request.method).toBe("GET");
const metricType =
req.request.params.get("metricType");
- switch(metricType) {
+ switch (metricType) {
case "tps_total":
req.flush({response:
totalResponse});
break;
@@ -416,32 +425,34 @@ describe("DeliveryServiceService", () => {
expect(req.request.params.get("endDate")).toBe(now.toISOString());
expect(req.request.method).toBe("GET");
- req.flush({response: {
- series: {
- columns: ["time", "mean"],
- count: 0,
- name: "invalid",
- values: [
- [
- twoSecondsAgo,
- null
+ req.flush({
+ response: {
+ series: {
+ columns: ["time",
"mean"],
+ count: 0,
+ name: "invalid",
+ values: [
+ [
+
twoSecondsAgo,
+ null
+ ],
+ [
+ now,
+ 0
+ ]
],
- [
- now,
- 0
- ]
- ],
- },
- summary: {
- average: 1,
- count: 2,
- fifthPercentile: 3,
- max: 4,
- min: 5,
- ninetyEightPercentile: 6,
- ninetyFifthPercentile: 7
+ },
+ summary: {
+ average: 1,
+ count: 2,
+ fifthPercentile: 3,
+ max: 4,
+ min: 5,
+ ninetyEightPercentile:
6,
+ ninetyFifthPercentile: 7
+ }
}
- }});
+ });
}
await expectAsync(responseP).toBeRejected();
@@ -570,6 +581,28 @@ describe("DeliveryServiceService", () => {
await expectAsync(responseP).toBeResolvedTo(response);
});
+ it("gets DS ssl keys", async () => {
+ let resp = service.getSSLKeys(testDS.xmlId);
+ let req = httpTestingController.expectOne(r => r.url ===
+
`/api/${service.apiVersion}/deliveryservices/xmlId/${testDS.xmlId}/sslkeys`);
+ expect(req.request.params.keys().length).toBe(1);
+ expect(req.request.params.get("decode")).toBe("true");
+ expect(req.request.method).toBe("GET");
+ req.flush({response: testDSSSLKeys});
+
+ await expectAsync(resp).toBeResolvedTo(testDSSSLKeys);
+
+ resp = service.getSSLKeys(testDS);
+ req = httpTestingController.expectOne(r => r.url ===
+
`/api/${service.apiVersion}/deliveryservices/xmlId/${testDS.xmlId}/sslkeys`);
+ expect(req.request.params.keys().length).toBe(1);
+ expect(req.request.params.get("decode")).toBe("true");
+ expect(req.request.method).toBe("GET");
+ req.flush({response: testDSSSLKeys});
+
+ await expectAsync(resp).toBeResolvedTo(testDSSSLKeys);
+ });
+
afterEach(() => {
httpTestingController.verify();
});
diff --git
a/experimental/traffic-portal/src/app/api/delivery-service.service.ts
b/experimental/traffic-portal/src/app/api/delivery-service.service.ts
index 8447f58c3d..714dfe9f03 100644
--- a/experimental/traffic-portal/src/app/api/delivery-service.service.ts
+++ b/experimental/traffic-portal/src/app/api/delivery-service.service.ts
@@ -20,7 +20,8 @@ import type {
RequestDeliveryService,
ResponseDeliveryService,
SteeringConfiguration,
- TypeFromResponse
+ TypeFromResponse,
+ ResponseDeliveryServiceSSLKey
} from "trafficops-types";
import type {
@@ -441,4 +442,16 @@ export class DeliveryServiceService extends APIService {
this.deliveryServiceTypes = r;
return r;
}
+
+ /**
+ * Gets a Delivery Service's SSL Keys
+ *
+ * @param ds The delivery service xmlid or object
+ * @returns The DS ssl keys
+ */
+ public async getSSLKeys(ds: string | ResponseDeliveryService):
Promise<ResponseDeliveryServiceSSLKey> {
+ const xmlId = typeof ds === "string" ? ds : ds.xmlId;
+ const path = `deliveryservices/xmlId/${xmlId}/sslkeys`;
+ return this.get<ResponseDeliveryServiceSSLKey>(path, undefined,
{decode: true}).toPromise();
+ }
}
diff --git
a/experimental/traffic-portal/src/app/api/testing/delivery-service.service.ts
b/experimental/traffic-portal/src/app/api/testing/delivery-service.service.ts
index e561aafa68..cee43c5219 100644
---
a/experimental/traffic-portal/src/app/api/testing/delivery-service.service.ts
+++
b/experimental/traffic-portal/src/app/api/testing/delivery-service.service.ts
@@ -17,6 +17,7 @@ import type {
Health,
RequestDeliveryService,
ResponseDeliveryService,
+ ResponseDeliveryServiceSSLKey,
SteeringConfiguration,
TypeFromResponse
} from "trafficops-types";
@@ -171,6 +172,13 @@ export class DeliveryServiceService {
useInTable: "deliveryservice"
}
];
+ private readonly dsSSLKeys: Array<ResponseDeliveryServiceSSLKey> = [{
+ cdn: "'",
+ certificate: {crt: "", csr: "", key: ""},
+ deliveryservice: "xml",
+ expiration: new Date(),
+ version: ""
+ }];
constructor(
private readonly cdnService: CDNService,
@@ -536,4 +544,20 @@ export class DeliveryServiceService {
public async getDSTypes(): Promise<Array<TypeFromResponse>> {
return this.dsTypes;
}
+
+ /**
+ * Gets a Delivery Service's SSL Keys
+ *
+ * @param ds The delivery service xmlid or object
+ * @returns The DS ssl keys
+ */
+ public async getSSLKeys(ds: string | ResponseDeliveryService):
Promise<ResponseDeliveryServiceSSLKey> {
+ const xmlId = typeof ds === "string" ? ds : ds.xmlId;
+ const key = this.dsSSLKeys.find(k => k.deliveryservice ===
xmlId);
+ if(!key) {
+ throw new Error(`no such Delivery Service: ${xmlId}`);
+ }
+
+ return key;
+ }
}
diff --git a/experimental/traffic-portal/src/app/app.ui.module.ts
b/experimental/traffic-portal/src/app/app.ui.module.ts
index 8fdb1a7bbc..d498c26d25 100644
--- a/experimental/traffic-portal/src/app/app.ui.module.ts
+++ b/experimental/traffic-portal/src/app/app.ui.module.ts
@@ -37,6 +37,7 @@ import { MatSidenavModule } from "@angular/material/sidenav";
import { MatSlideToggleModule } from "@angular/material/slide-toggle";
import { MatSnackBarModule } from "@angular/material/snack-bar";
import { MatStepperModule } from "@angular/material/stepper";
+import { MatTabsModule } from "@angular/material/tabs";
import { MatToolbarModule } from "@angular/material/toolbar";
import { MatTooltipModule } from "@angular/material/tooltip";
import { MatTreeModule } from "@angular/material/tree";
@@ -74,6 +75,7 @@ import { AgGridModule } from "ag-grid-angular";
MatSidenavModule,
MatSnackBarModule,
MatStepperModule,
+ MatTabsModule,
MatToolbarModule,
MatTooltipModule,
MatTreeModule,
diff --git
a/experimental/traffic-portal/src/app/core/certs/cert-author/cert-author.component.html
b/experimental/traffic-portal/src/app/core/certs/cert-author/cert-author.component.html
new file mode 100644
index 0000000000..20ded9b2b3
--- /dev/null
+++
b/experimental/traffic-portal/src/app/core/certs/cert-author/cert-author.component.html
@@ -0,0 +1,41 @@
+<!--
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<div *ngIf="author" class="content">
+ <mat-form-field>
+ <mat-label>Common Name</mat-label>
+ <input matInput type="text" name="commonName" readonly
[value]="author.commonName"/>
+ </mat-form-field>
+ <div class="triple">
+ <mat-form-field>
+ <mat-label>Country Name</mat-label>
+ <input matInput type="text" name="country" readonly
[value]="author.countryName ?? ''"/>
+ </mat-form-field>
+ <mat-form-field>
+ <mat-label>State or Province</mat-label>
+ <input matInput type="text" name="state" readonly
[value]="author.stateOrProvince ?? ''"/>
+ </mat-form-field>
+ <mat-form-field>
+ <mat-label>Locality</mat-label>
+ <input matInput type="text" name="local" readonly
[value]="author.localityName ?? ''"/>
+ </mat-form-field>
+ </div>
+ <mat-form-field>
+ <mat-label>Organization</mat-label>
+ <input matInput type="text" name="org" readonly
[value]="author.orgName ?? ''"/>
+ </mat-form-field>
+ <mat-form-field>
+ <mat-label>Organization Unit</mat-label>
+ <input matInput type="text" name="org" readonly
[value]="author.orgUnit ?? ''"/>
+ </mat-form-field>
+</div>
diff --git
a/experimental/traffic-portal/src/app/core/certs/cert-author/cert-author.component.scss
b/experimental/traffic-portal/src/app/core/certs/cert-author/cert-author.component.scss
new file mode 100644
index 0000000000..548a34fd32
--- /dev/null
+++
b/experimental/traffic-portal/src/app/core/certs/cert-author/cert-author.component.scss
@@ -0,0 +1,22 @@
+/*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+div.content {
+ display: grid;
+
+ .triple {
+ display: grid;
+ grid-template-columns: 1fr 2fr 2fr;
+ grid-column-gap: 10px;
+ }
+}
diff --git
a/experimental/traffic-portal/src/app/core/certs/cert-author/cert-author.component.spec.ts
b/experimental/traffic-portal/src/app/core/certs/cert-author/cert-author.component.spec.ts
new file mode 100644
index 0000000000..101af9b524
--- /dev/null
+++
b/experimental/traffic-portal/src/app/core/certs/cert-author/cert-author.component.spec.ts
@@ -0,0 +1,51 @@
+/*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+import { HarnessLoader } from "@angular/cdk/testing";
+import { TestbedHarnessEnvironment } from "@angular/cdk/testing/testbed";
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { MatFormFieldHarness } from "@angular/material/form-field/testing";
+import { NoopAnimationsModule } from "@angular/platform-browser/animations";
+
+import { AppUIModule } from "src/app/app.ui.module";
+
+import { CertAuthorComponent } from "./cert-author.component";
+
+describe("CertAuthorComponent", () => {
+ let component: CertAuthorComponent;
+ let fixture: ComponentFixture<CertAuthorComponent>;
+ let loader: HarnessLoader;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ CertAuthorComponent ],
+ imports: [ AppUIModule, NoopAnimationsModule ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(CertAuthorComponent);
+ component = fixture.componentInstance;
+ component.author = {
+ commonName: "name"
+ };
+ fixture.detectChanges();
+ loader = TestbedHarnessEnvironment.documentRootLoader(fixture);
+ });
+
+ it("should create", async () => {
+ expect(component).toBeTruthy();
+ await fixture.whenRenderingDone();
+ const formFields = await
loader.getAllHarnesses(MatFormFieldHarness);
+ expect(formFields.length).toBe(6);
+ });
+});
diff --git
a/experimental/traffic-portal/src/app/core/certs/cert-author/cert-author.component.ts
b/experimental/traffic-portal/src/app/core/certs/cert-author/cert-author.component.ts
new file mode 100644
index 0000000000..6ad94112e6
--- /dev/null
+++
b/experimental/traffic-portal/src/app/core/certs/cert-author/cert-author.component.ts
@@ -0,0 +1,30 @@
+/*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+import { Component, Input } from "@angular/core";
+
+import { type Author } from
"src/app/core/certs/cert-detail/cert-detail.component";
+
+/**
+ * CertAuthorComponent is the controller used for displaying a cert author.
+ */
+@Component({
+ selector: "tp-cert-author",
+ styleUrls: ["./cert-author.component.scss"],
+ templateUrl: "./cert-author.component.html"
+})
+export class CertAuthorComponent {
+ @Input({required: true}) public author!: Author;
+
+}
diff --git
a/experimental/traffic-portal/src/app/core/certs/cert-detail/cert-detail.component.html
b/experimental/traffic-portal/src/app/core/certs/cert-detail/cert-detail.component.html
new file mode 100644
index 0000000000..d1994158fd
--- /dev/null
+++
b/experimental/traffic-portal/src/app/core/certs/cert-detail/cert-detail.component.html
@@ -0,0 +1,68 @@
+<!--
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<div>
+ <div class="mat-elevation-z2 interior-container">
+ <h2>Issuer</h2>
+ <tp-cert-author [author]="issuer"></tp-cert-author>
+ </div>
+ <div class="mat-elevation-z2 interior-container">
+ <h2>Subject</h2>
+ <tp-cert-author [author]="subject"></tp-cert-author>
+ </div>
+ <div class="mat-elevation-z2 interior-container">
+ <h2>Validity</h2>
+ <div class="content">
+ <div class="double">
+ <mat-form-field>
+ <mat-label>Not Valid Before</mat-label>
+ <input matInput type="datetime-local"
[formControl]="validBeforeFormControl" name="validBefore" readonly/>
+ <mat-error
*ngIf="validBeforeFormControl.hasError('outOfDate')">Not currently
valid</mat-error>
+ </mat-form-field>
+ <mat-form-field>
+ <mat-label>Not Valid After</mat-label>
+ <input matInput type="datetime-local"
[formControl]="validAfterFormControl" name="validAfter" readonly/>
+ <mat-error
*ngIf="validAfterFormControl.hasError('outOfDate')">Not currently
valid</mat-error>
+ </mat-form-field>
+ </div>
+ </div>
+ </div>
+ <div class="mat-elevation-z2 interior-container">
+ <h2>Misc</h2>
+ <div class="content">
+ <div class="double"></div>
+ <mat-form-field>
+ <mat-label>Signature Algorithm</mat-label>
+ <input matInput type="text" name="sigType"
readonly [value]="oidToName(cert.signatureOid)"/>
+ </mat-form-field>
+ <div class="squished">
+ <mat-form-field>
+ <mat-label>Version</mat-label>
+ <input matInput type="number"
name="version" readonly [value]="cert.version.toString()"/>
+ </mat-form-field>
+ <mat-form-field>
+ <mat-label>Serial Number</mat-label>
+ <input matInput type="text"
name="serialNumber" readonly [value]="cert.serialNumber"/>
+ </mat-form-field>
+ </div>
+ <mat-form-field>
+ <mat-label>SHA 256</mat-label>
+ <input matInput type="text" name="sha256"
readonly [value]="sha256"/>
+ </mat-form-field>
+ <mat-form-field>
+ <mat-label>SHA 1</mat-label>
+ <input matInput type="text" name="sha1"
readonly [value]="sha1"/>
+ </mat-form-field>
+ </div>
+ </div>
+</div>
diff --git
a/experimental/traffic-portal/src/app/core/certs/cert-detail/cert-detail.component.scss
b/experimental/traffic-portal/src/app/core/certs/cert-detail/cert-detail.component.scss
new file mode 100644
index 0000000000..177190a43a
--- /dev/null
+++
b/experimental/traffic-portal/src/app/core/certs/cert-detail/cert-detail.component.scss
@@ -0,0 +1,34 @@
+/*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+div.interior-container {
+ padding: .5em 1em;
+ margin-bottom: .5em;
+
+ div.content {
+ display: grid;
+
+ .double {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ grid-column-gap: 10px;
+ }
+
+ .squished {
+ display: grid;
+ grid-template-columns: 1fr 3fr;
+ grid-column-gap: 10px;
+ }
+ }
+}
diff --git
a/experimental/traffic-portal/src/app/core/certs/cert-detail/cert-detail.component.spec.ts
b/experimental/traffic-portal/src/app/core/certs/cert-detail/cert-detail.component.spec.ts
new file mode 100644
index 0000000000..7406df6e96
--- /dev/null
+++
b/experimental/traffic-portal/src/app/core/certs/cert-detail/cert-detail.component.spec.ts
@@ -0,0 +1,167 @@
+/*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { pki } from "node-forge";
+
+import { pkiCertToSHA1, pkiCertToSHA256 } from "src/app/core/certs/cert.util";
+
+import { CertDetailComponent } from "./cert-detail.component";
+
+const certPEM = `
+-----BEGIN CERTIFICATE-----
+MIIDeTCCAmECFDWSnKTtkcoRnoTz6ChHqTuvCUPHMA0GCSqGSIb3DQEBCwUAMHkx
+CzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDTzEPMA0GA1UEBwwGRGVudmVyMQ8wDQYD
+VQQKDAZBcGFjaGUxGDAWBgNVBAsMD1RyYWZmaWMgQ29udHJvbDEhMB8GA1UEAwwY
+dHJhZmZpY29wcy5kZXYuY2lhYi50ZXN0MB4XDTIzMDYwNTE0MDY0OFoXDTI0MDYw
+NDE0MDY0OFoweTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNPMQ8wDQYDVQQHDAZE
+ZW52ZXIxDzANBgNVBAoMBkFwYWNoZTEYMBYGA1UECwwPVHJhZmZpYyBDb250cm9s
+MSEwHwYDVQQDDBh0cmFmZmljb3BzLmRldi5jaWFiLnRlc3QwggEiMA0GCSqGSIb3
+DQEBAQUAA4IBDwAwggEKAoIBAQDKAcQK9fe9w7p7eMnygnlV0rlbUdVr9DEQpKym
+Ul7zGj9/Ta3n0h8xWrmmMi2ZJnIUI4AV7HKaYXiAke1rbEx2jAdvXdjNm/S7RORy
+M0piJc8Si4/EJI1sZU17kZ7howXJvAMCQBqcI+hG93ATlUIOoYuluX7wSNIMw1Np
+lT5bcmVDf5nVQGnrPw22mCGjH5JBxW5i1DjCoNovHfFgNmwP6y8C1jygoMPL+rxl
+sq8fyUE/+qtcEkjUrr4oi9kjTESDqHghrkejKk6NPlPi97SDz2Ffdagoq2aqBhw9
+P86JgplPVHHMWOLXBww0wPAClqY8H7CIt5rgZzoWmoR0DjjNAgMBAAEwDQYJKoZI
+hvcNAQELBQADggEBAMFz7k+egg+hP86ylEAuUfcy/beO3Pf3Fn7oMh5MDENfOzON
+IFqZOQ8pN1zfoAx0rRTzYHcg/AZs2AA4oh+WyEKHDrmICGfsF481b6A0EarZ/cRy
+MF3Vh5rTd8ujWT4V9GP3Hc/I3F5tUKxPWiVEKTVRr6wzjwtXctOnhcbB3FeRtGDY
+CfVBYMSEDJmAyMchfST/GwdG46Ak2TSaMpOf6tL5aMw+xfmDI68JGwG0LNliyEoW
+xOHRCtWd5Q+Sn3rgx4h6nzdZOGHw3HwDbsX/y/dZNc7luUImEWwTyhohnO9XqaBX
+EsdMDJmBaoVum+sR6ch08TsqrTHAfdB3xJF37Wc=
+-----END CERTIFICATE-----`;
+
+describe("CertDetailComponent", () => {
+ let component: CertDetailComponent;
+ let cert: pki.Certificate;
+ let fixture: ComponentFixture<CertDetailComponent>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [CertDetailComponent]
+ })
+ .compileComponents();
+
+ cert = pki.certificateFromPem(certPEM);
+ cert.setIssuer([
+ {
+ name: "countryName",
+ shortName: "C",
+ type: "2.5.4.6",
+ value: "C",
+ valueTagClass: 19,
+ },
+ {
+ name: "stateOrProvinceName",
+ shortName: "ST",
+ type: "2.5.4.8",
+ value: "ST",
+ valueTagClass: 19,
+ },
+ {
+ name: "localityName",
+ shortName: "L",
+ type: "2.5.4.7",
+ value: "L",
+ valueTagClass: 19,
+ },
+ {
+ name: "organizationName",
+ shortName: "O",
+ type: "2.5.4.10",
+ value: "O",
+ valueTagClass: 19,
+ },
+ {
+ name: "commonName",
+ shortName: "CN",
+ type: "2.5.4.3",
+ value: "CN",
+ valueTagClass: 19,
+ },
+ {
+ name: "madeUp",
+ shortName: "idksomething",
+ type: "127.0.0.1",
+ value: "doesntmatter",
+ }
+ ]);
+ cert.setSubject([
+ {
+ name: "countryName",
+ shortName: "C",
+ type: "2.5.4.6",
+ value: "C",
+ valueTagClass: 19,
+ },
+ {
+ name: "stateOrProvinceName",
+ shortName: "ST",
+ type: "2.5.4.8",
+ value: "ST",
+ valueTagClass: 19,
+ },
+ {
+ name: "localityName",
+ shortName: "L",
+ type: "2.5.4.7",
+ value: "L",
+ valueTagClass: 19,
+ },
+ {
+ name: "organizationName",
+ shortName: "O",
+ type: "2.5.4.10",
+ value: "O",
+ valueTagClass: 19,
+ },
+ {
+ name: "commonName",
+ shortName: "CN",
+ type: "2.5.4.3",
+ value: "CN",
+ valueTagClass: 19,
+ }
+
+ ]);
+
+ fixture = TestBed.createComponent(CertDetailComponent);
+ component = fixture.componentInstance;
+ component.cert = cert;
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("Fields calculated correctly", async () => {
+ component.ngOnChanges();
+ expect(component.issuer).toEqual({
+ commonName: "CN",
+ countryName: "C",
+ localityName: "L",
+ orgName: "O",
+ stateOrProvince: "ST",
+ });
+ expect(component.subject).toEqual({
+ commonName: "CN",
+ countryName: "C",
+ localityName: "L",
+ orgName: "O",
+ stateOrProvince: "ST",
+ });
+ expect(component.sha1).toBe(pkiCertToSHA1(component.cert));
+ expect(component.sha256).toBe(pkiCertToSHA256(component.cert));
+ });
+});
diff --git
a/experimental/traffic-portal/src/app/core/certs/cert-detail/cert-detail.component.ts
b/experimental/traffic-portal/src/app/core/certs/cert-detail/cert-detail.component.ts
new file mode 100644
index 0000000000..8f7a6c748c
--- /dev/null
+++
b/experimental/traffic-portal/src/app/core/certs/cert-detail/cert-detail.component.ts
@@ -0,0 +1,133 @@
+/*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+import { Component, Input, OnChanges } from "@angular/core";
+import { AbstractControl, FormControl, ValidationErrors, ValidatorFn } from
"@angular/forms";
+import { pki, Hex } from "node-forge";
+
+import { oidToName, pkiCertToSHA1, pkiCertToSHA256 } from
"src/app/core/certs/cert.util";
+
+/**
+ * Author contains the information about an author from a cert issuer/subject
+ */
+export interface Author {
+ countryName?: string | undefined;
+ stateOrProvince?: string | undefined;
+ localityName?: string | undefined;
+ orgName?: string | undefined;
+ orgUnit?: string | undefined;
+ commonName: string;
+}
+
+/**
+ * Angular validator that checks if the value is either before or after the
given time
+ *
+ * @param before If this is only valid before
+ * @param now The time to compare against
+ * @returns Validator Function
+ */
+function createDateValidator(before: boolean, now: Date): ValidatorFn {
+ return (control: AbstractControl): ValidationErrors | null => {
+ const value = control.value;
+ if (!value) {
+ return null;
+ }
+
+ let valid = false;
+ const d = new Date(value);
+ if (before) {
+ valid = now >= d;
+ } else {
+ valid = now <= d;
+ }
+
+ return valid ? null : {outOfDate: true};
+ };
+}
+
+/**
+ * Controller for the Cert Detail component
+ */
+@Component({
+ selector: "tp-cert-detail",
+ styleUrls: ["./cert-detail.component.scss"],
+ templateUrl: "./cert-detail.component.html"
+})
+export class CertDetailComponent implements OnChanges {
+ @Input({required: true}) public cert!: pki.Certificate;
+
+ public issuer: Author = {commonName: ""};
+ public subject: Author = {commonName: ""};
+ public now: Date = new Date();
+ public validAfterFormControl = new FormControl<string>("",
[createDateValidator(false, this.now)]);
+ public validBeforeFormControl = new FormControl<string>("",
[createDateValidator(true, this.now)]);
+
+ public sha1: Hex = "";
+ public sha256: Hex = "";
+
+ /**
+ * processAttributes converts attributes into an author
+ *
+ * @param attrs The attributes to process
+ * @returns The resultant author
+ */
+ public processAttributes(attrs: pki.CertificateField[]): Author {
+ const a: Author = {commonName: ""};
+ for (const attr of attrs) {
+ if (attr.name && attr.value) {
+ if (typeof attr.value !== "string") {
+ console.warn(`Unknown attribute value
${attr.value}`);
+ continue;
+ }
+ switch (attr.name) {
+ case "commonName":
+ a.commonName = attr.value;
+ break;
+ case "countryName":
+ a.countryName = attr.value;
+ break;
+ case "stateOrProvinceName":
+ a.stateOrProvince = attr.value;
+ break;
+ case "localityName":
+ a.localityName = attr.value;
+ break;
+ case "organizationName":
+ a.orgName = attr.value;
+ break;
+ case "organizationUnitName":
+ a.orgUnit = attr.value;
+ break;
+ }
+ }
+ }
+ return a;
+ }
+
+ /**
+ * Calculates certificate details
+ */
+ public ngOnChanges(): void {
+ this.sha1 = pkiCertToSHA1(this.cert);
+ this.sha256 = pkiCertToSHA256(this.cert);
+ this.issuer =
this.processAttributes(this.cert.issuer.attributes);
+ this.subject =
this.processAttributes(this.cert.subject.attributes);
+
+
this.validBeforeFormControl.setValue(this.cert.validity.notBefore.toISOString().slice(0,
16));
+
this.validAfterFormControl.setValue(this.cert.validity.notAfter.toISOString().slice(0,
16));
+ this.validAfterFormControl.markAsTouched();
+ this.validBeforeFormControl.markAsTouched();
+ }
+
+ protected readonly oidToName = oidToName;
+}
diff --git
a/experimental/traffic-portal/src/app/core/certs/cert-viewer/cert-viewer.component.html
b/experimental/traffic-portal/src/app/core/certs/cert-viewer/cert-viewer.component.html
new file mode 100644
index 0000000000..c831b29220
--- /dev/null
+++
b/experimental/traffic-portal/src/app/core/certs/cert-viewer/cert-viewer.component.html
@@ -0,0 +1,77 @@
+<!--
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<mat-card appearance="outlined" class="page-content">
+ <tp-loading *ngIf="dsCert === undefined"></tp-loading>
+ <mat-card-content *ngIf="dsCert !== undefined">
+ <mat-tab-group #matTab>
+ <mat-tab label="Input Certificate" *ngIf="!dsCert">
+ <form ngNativeValidate
(submit)="process(true)" class="tab-content">
+ <mat-form-field>
+ <mat-label>Certificate
Chain</mat-label>
+ <textarea matInput
name="custom" [(ngModel)]="inputCert" cdkAutosizeMaxRows="15"
cdkTextareaAutosize required></textarea>
+ </mat-form-field>
+ <button mat-raised-button
type="submit">Submit</button>
+ </form>
+ </mat-tab>
+ <mat-tab label="Certificate Details"
[disabled]="certChain.length === 0">
+ <div class="tab-content">
+ <mat-form-field>
+ <mat-label>Detected
Order</mat-label>
+ <input matInput readonly
name="order" [(ngModel)]="certOrder"/>
+ </mat-form-field>
+ <mat-accordion>
+ <mat-expansion-panel
*ngFor="let c of certChain; let i = index;">
+
<mat-expansion-panel-header>{{c.type}}</mat-expansion-panel-header>
+ <tp-cert-detail
*ngIf="!c.parseError" [cert]="c"></tp-cert-detail>
+ <div
*ngIf="c.parseError">Unable to parse this certificate!</div>
+ </mat-expansion-panel>
+ </mat-accordion>
+ </div>
+ </mat-tab>
+ <mat-tab label="TO Cert Info" *ngIf="dsCert">
+ <div class="tab-content">
+ <mat-form-field>
+ <mat-label>Version</mat-label>
+ <input type="text" matInput
name="version" [(ngModel)]="cert.version" />
+ </mat-form-field>
+ <mat-form-field>
+ <mat-label>Common
Name</mat-label>
+ <input type="text" matInput
name="commonName" [(ngModel)]="cert.hostname" />
+ </mat-form-field>
+ <mat-form-field>
+
<mat-label>Expiration</mat-label>
+ <input type="text" matInput
name="expiration" [(ngModel)]="cert.expiration" />
+ </mat-form-field>
+ <mat-form-field>
+ <mat-label>SANs</mat-label>
+ <input type="text" matInput
name="sans" [(ngModel)]="cert.sans" />
+ </mat-form-field>
+ <mat-form-field>
+ <mat-label>Private
Key</mat-label>
+ <textarea matInput
class="private-text" name="privateKey" [(ngModel)]="cert.certificate.key"
cdkAutosizeMaxRows="15" cdkTextareaAutosize></textarea>
+ </mat-form-field>
+ <mat-form-field>
+ <mat-label>Certificate Signing
Request</mat-label>
+ <textarea matInput name="csr"
[(ngModel)]="cert.certificate.csr" cdkAutosizeMaxRows="15"
cdkTextareaAutosize></textarea>
+ </mat-form-field>
+ <mat-form-field>
+
<mat-label>Certificate</mat-label>
+ <textarea matInput name="crt"
[(ngModel)]="cert.certificate.crt" cdkAutosizeMaxRows="15"
cdkTextareaAutosize></textarea>
+ </mat-form-field>
+ </div>
+ </mat-tab>
+ </mat-tab-group>
+ </mat-card-content>
+</mat-card>
diff --git
a/experimental/traffic-portal/src/app/core/certs/cert-viewer/cert-viewer.component.scss
b/experimental/traffic-portal/src/app/core/certs/cert-viewer/cert-viewer.component.scss
new file mode 100644
index 0000000000..2042ec0968
--- /dev/null
+++
b/experimental/traffic-portal/src/app/core/certs/cert-viewer/cert-viewer.component.scss
@@ -0,0 +1,35 @@
+/*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+.tab-content {
+ margin-top: 5px;
+ display: grid;
+
+ &:is(form), .mat-accordion {
+ width: 98%;
+ margin: auto auto 15px;
+ }
+}
+
+.private-text {
+ &:not(:focus) {
+ color: transparent;
+ text-shadow: 0 0 8px rgba(0, 0, 0, 0.5);
+ }
+
+ &:focus {
+ color: inherit;
+ text-shadow: none;
+ }
+}
diff --git
a/experimental/traffic-portal/src/app/core/certs/cert-viewer/cert-viewer.component.spec.ts
b/experimental/traffic-portal/src/app/core/certs/cert-viewer/cert-viewer.component.spec.ts
new file mode 100644
index 0000000000..398a26b81b
--- /dev/null
+++
b/experimental/traffic-portal/src/app/core/certs/cert-viewer/cert-viewer.component.spec.ts
@@ -0,0 +1,161 @@
+/*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { NoopAnimationsModule } from "@angular/platform-browser/animations";
+import { RouterTestingModule } from "@angular/router/testing";
+import * as forge from "node-forge";
+
+import { APITestingModule } from "src/app/api/testing";
+import { AppUIModule } from "src/app/app.ui.module";
+
+import { CertViewerComponent } from "./cert-viewer.component";
+
+/**
+ * Creates any type of cert in a chain
+ *
+ * @param validStart Not valid before time
+ * @param validEnd Not valid after time
+ * @param issuer The issuer attributes
+ * @param subject The subject attributes
+ * @param extensions The cert extensions
+ * @returns The cert
+ */
+export function createCert(validStart: Date, validEnd: Date, issuer:
forge.pki.CertificateField[],
+ subject: forge.pki.CertificateField[], extensions: unknown[]):
forge.pki.Certificate {
+ const kp = forge.pki.rsa.generateKeyPair(2048);
+ const cert = forge.pki.createCertificate();
+ cert.publicKey = kp.publicKey;
+ cert.privateKey = kp.privateKey;
+ cert.validity.notBefore = validStart;
+ cert.validity.notAfter = validEnd;
+ cert.setSubject(subject);
+ cert.setIssuer(issuer);
+ cert.setExtensions(extensions);
+ cert.sign(kp.privateKey, forge.md.sha256.create());
+ return cert;
+}
+
+/**
+ * Creates a certificate and signs it using a CA
+ *
+ * @param validStart Not valid before time
+ * @param validEnd Not valid after time
+ * @param subject Cert subject
+ * @param ca The CA that issued this cert
+ * @returns The cert
+ */
+export function createCertAndSign(validStart: Date, validEnd: Date, subject:
forge.pki.CertificateField[],
+ ca: forge.pki.Certificate): forge.pki.Certificate {
+ const cert = createCert(validStart, validEnd, ca.issuer.attributes,
subject, []);
+ cert.sign(ca.privateKey, forge.md.sha256.create());
+ return cert;
+}
+
+/**
+ * Creates a Certificate Authority
+ *
+ * @param validStart Not valid before time
+ * @param validEnd Not valid after time
+ * @param attrs Both subject & issuer attributes
+ * @returns The CA
+ */
+export function createCA(validStart: Date, validEnd: Date, attrs:
forge.pki.CertificateField[]): forge.pki.Certificate {
+ return createCert(validStart, validEnd, attrs, attrs,
+ [{
+ cA: true,
+ name: "basicConstraints"
+ }, {
+ keyCertSign: true,
+ name: "keyUsage"
+ }, {
+ name: "nsCertType",
+ server: true
+ }]);
+}
+
+describe("CertViewerComponent", () => {
+ let component: CertViewerComponent;
+ let fixture: ComponentFixture<CertViewerComponent>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [CertViewerComponent],
+ imports: [APITestingModule, AppUIModule,
NoopAnimationsModule, RouterTestingModule]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(CertViewerComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ expect(component.dsCert).toBe(false);
+ });
+
+ it("new cert", () => {
+ component.inputCert = "invalid";
+ component.process(true);
+ expect(component.certChain.length).toBe(1);
+ expect(component.certChain[0].parseError).toBeTrue();
+ expect(component.certChain[0].type).toBe("Error");
+ expect(component.certOrder).toBe("Unknown");
+ });
+
+ it("good cert", () => {
+ const today = new Date();
+ component.inputCert =
forge.pki.certificateToPem(createCert(today, today, [{
+ name: "commonName",
+ value: "Test"
+ }], [{
+ name: "commonName",
+ value: "Test"
+ }], []));
+ component.process(true);
+ expect(component.certChain.length).toBe(1);
+ expect(component.certChain[0].parseError).toBeFalsy();
+ expect(component.certChain[0].type).toBe("Root");
+ expect(component.certOrder).toBe("Single");
+
+ });
+
+ it("root chain", () => {
+ const today = new Date();
+ const ca = createCA(today, today, [{
+ name: "commonName",
+ value: "Test"
+ }]);
+ const cert = createCertAndSign(today, today, [{
+ name: "commonName",
+ value: "Test2"
+ }], ca);
+
+ component.inputCert =
`${forge.pki.certificateToPem(ca)}${forge.pki.certificateToPem(cert)}`;
+ component.process(true);
+ expect(component.certChain.length).toBe(2);
+ expect(component.certChain.some(c => c.parseError)).toBeFalse();
+ expect(component.certChain[0].type).toBe("Root");
+ expect(component.certChain[1].type).toBe("Client");
+ expect(component.certOrder).toBe("Root -> Client");
+
+ component.inputCert =
`${forge.pki.certificateToPem(cert)}${forge.pki.certificateToPem(ca)}`;
+ component.process(true);
+ expect(component.certChain.length).toBe(2);
+ expect(component.certChain.some(c => c.parseError)).toBeFalse();
+ expect(component.certChain[0].type).toBe("Root");
+ expect(component.certChain[1].type).toBe("Client");
+ expect(component.certOrder).toBe("Client -> Root");
+ });
+});
diff --git
a/experimental/traffic-portal/src/app/core/certs/cert-viewer/cert-viewer.component.ts
b/experimental/traffic-portal/src/app/core/certs/cert-viewer/cert-viewer.component.ts
new file mode 100644
index 0000000000..0276e73f7c
--- /dev/null
+++
b/experimental/traffic-portal/src/app/core/certs/cert-viewer/cert-viewer.component.ts
@@ -0,0 +1,193 @@
+/*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+import { Component, OnInit, ViewChild } from "@angular/core";
+import { FormControl } from "@angular/forms";
+import { MatTabGroup } from "@angular/material/tabs";
+import { ActivatedRoute, Router } from "@angular/router";
+import { pki } from "node-forge";
+import { type ResponseDeliveryServiceSSLKey } from "trafficops-types";
+
+import { DeliveryServiceService } from "src/app/api";
+
+/**
+ * What type of cert is it
+ */
+type CertType = "Root" | "Client" | "Intermediate" | "Unknown" | "Error";
+
+/**
+ * Detected order of the cert chain
+ */
+type CertOrder = "Client -> Root" | "Root -> Client" | "Unknown" | "Single";
+
+/**
+ * Wrapper around Certificate that contains additional fields
+ */
+export interface AugmentedCertificate extends pki.Certificate {
+ type: CertType;
+ parseError: boolean;
+}
+
+export const NULL_CERT = pki.createCertificate() as AugmentedCertificate;
+NULL_CERT.type = "Error";
+NULL_CERT.parseError = true;
+
+/**
+ * Controller for the Cert Viewer component.
+ */
+@Component({
+ selector: "tp-cert-viewer",
+ styleUrls: ["./cert-viewer.component.scss"],
+ templateUrl: "./cert-viewer.component.html"
+})
+export class CertViewerComponent implements OnInit {
+ public cert!: ResponseDeliveryServiceSSLKey;
+ public inputCert = "";
+ public dsCert = false;
+
+ public certChain: Array<AugmentedCertificate> = [];
+ public certOrder: CertOrder | undefined;
+ public privateKeyFormControl = new FormControl("");
+
+ @ViewChild("matTab") public matTab!: MatTabGroup;
+ constructor(
+ private readonly route: ActivatedRoute,
+ private readonly dsAPI: DeliveryServiceService,
+ private readonly router: Router) {
+ }
+
+ /**
+ * newCert creates a cert from an input string.
+ *
+ * @param input The text to read as a cert
+ * @private
+ * @returns Resultant Cert
+ */
+ private newCert(input: string): AugmentedCertificate {
+ try {
+ return pki.certificateFromPem(input) as
AugmentedCertificate;
+ } catch (e) {
+ console.error(`ran into issue creating certificate from
input ${input}`, e);
+ return NULL_CERT;
+ }
+ }
+
+ /**
+ * process takes the Cert Chain text input and parses it.
+ *
+ * @param uploaded if the certificate was uploaded by the client.
+ */
+ public process(uploaded: boolean = false): void {
+ this.inputCert = this.inputCert.replace(/\r\n/g, "\n");
+ const parts = this.inputCert.split("-\n-");
+ const certs = new Array<AugmentedCertificate>(parts.length);
+ for(let i = 1; i < parts.length; ++i) {
+ parts[i-1] += "-";
+ parts[i] = `-${parts[i]}`;
+ certs[i-1] = this.newCert(parts[i - 1]);
+ }
+ certs[certs.length-1] = this.newCert(parts[parts.length - 1]);
+ const assignType = (c: AugmentedCertificate, i: number): void
=> {
+ if(c.parseError) {
+ return;
+ }
+ if (i === 0) {
+ c.type = "Root";
+ } else if (i === certs.length - 1) {
+ c.type = "Client";
+ } else {
+ c.type = "Intermediate";
+ }
+ };
+ const chain = this.reOrderRootFirst(certs);
+ chain.forEach(assignType);
+ this.certChain = chain;
+
+ if(this.matTab && uploaded) {
+ this.matTab.selectedIndex = 1;
+ }
+ }
+
+ /**
+ * reOrderRootFirst sorts a cert chain with the root being first if
possible.
+ *
+ * @param certs The list of certs to reorder
+ * @returns The processed certs
+ */
+ public reOrderRootFirst(certs: Array<AugmentedCertificate>):
Array<AugmentedCertificate> {
+ let rootFirst = false;
+ let invalid = false;
+ for(let i = 1; i < certs.length; ++i){
+ const first = certs[i-1];
+ const next = certs[i];
+ if(first.parseError) {
+ invalid = true;
+ continue;
+ } else if (next.parseError) {
+ invalid = true;
+ continue;
+ }
+ if (first.issued(next)) {
+ rootFirst = true;
+ } else if (next.issued(first)) {
+ rootFirst = false;
+ } else {
+ invalid = true;
+ console.error(`Cert chain is invalid, cert
${i-1} and ${i} are not related`);
+ }
+ }
+
+ if (certs.length === 1) {
+ if (certs[0].parseError) {
+ invalid = true;
+ } else {
+ this.certOrder = "Single";
+ return certs;
+ }
+ }
+ if (invalid) {
+ this.certOrder = "Unknown";
+ return certs;
+ }
+
+ if(rootFirst) {
+ this.certOrder = "Root -> Client";
+ return certs;
+ }
+ this.certOrder = "Client -> Root";
+ certs = certs.reverse();
+ return certs;
+ }
+
+ /**
+ * Checks if we are a DS cert or any user provided cert.
+ */
+ public async ngOnInit(): Promise<void> {
+ const ID = this.route.snapshot.paramMap.get("xmlId");
+ if (ID === null) {
+ this.dsCert = false;
+ return;
+ }
+ try {
+ this.cert = await this.dsAPI.getSSLKeys(ID);
+ } catch {
+ await this.router.navigate(["/core/certs/ssl/"]);
+ return;
+ }
+ this.dsCert = true;
+ this.inputCert = this.cert.certificate.crt;
+ this.privateKeyFormControl.setValue(this.cert.certificate.key);
+ this.process();
+ }
+
+}
diff --git a/experimental/traffic-portal/src/app/core/certs/cert.util.spec.ts
b/experimental/traffic-portal/src/app/core/certs/cert.util.spec.ts
new file mode 100644
index 0000000000..a8fcc24bf8
--- /dev/null
+++ b/experimental/traffic-portal/src/app/core/certs/cert.util.spec.ts
@@ -0,0 +1,55 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import * as forge from "node-forge";
+
+import { oidToName, pkiCertToSHA1, pkiCertToSHA256 } from
"src/app/core/certs/cert.util";
+
+const certPEM = `
+-----BEGIN CERTIFICATE-----
+MIIDeTCCAmECFDWSnKTtkcoRnoTz6ChHqTuvCUPHMA0GCSqGSIb3DQEBCwUAMHkx
+CzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDTzEPMA0GA1UEBwwGRGVudmVyMQ8wDQYD
+VQQKDAZBcGFjaGUxGDAWBgNVBAsMD1RyYWZmaWMgQ29udHJvbDEhMB8GA1UEAwwY
+dHJhZmZpY29wcy5kZXYuY2lhYi50ZXN0MB4XDTIzMDYwNTE0MDY0OFoXDTI0MDYw
+NDE0MDY0OFoweTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNPMQ8wDQYDVQQHDAZE
+ZW52ZXIxDzANBgNVBAoMBkFwYWNoZTEYMBYGA1UECwwPVHJhZmZpYyBDb250cm9s
+MSEwHwYDVQQDDBh0cmFmZmljb3BzLmRldi5jaWFiLnRlc3QwggEiMA0GCSqGSIb3
+DQEBAQUAA4IBDwAwggEKAoIBAQDKAcQK9fe9w7p7eMnygnlV0rlbUdVr9DEQpKym
+Ul7zGj9/Ta3n0h8xWrmmMi2ZJnIUI4AV7HKaYXiAke1rbEx2jAdvXdjNm/S7RORy
+M0piJc8Si4/EJI1sZU17kZ7howXJvAMCQBqcI+hG93ATlUIOoYuluX7wSNIMw1Np
+lT5bcmVDf5nVQGnrPw22mCGjH5JBxW5i1DjCoNovHfFgNmwP6y8C1jygoMPL+rxl
+sq8fyUE/+qtcEkjUrr4oi9kjTESDqHghrkejKk6NPlPi97SDz2Ffdagoq2aqBhw9
+P86JgplPVHHMWOLXBww0wPAClqY8H7CIt5rgZzoWmoR0DjjNAgMBAAEwDQYJKoZI
+hvcNAQELBQADggEBAMFz7k+egg+hP86ylEAuUfcy/beO3Pf3Fn7oMh5MDENfOzON
+IFqZOQ8pN1zfoAx0rRTzYHcg/AZs2AA4oh+WyEKHDrmICGfsF481b6A0EarZ/cRy
+MF3Vh5rTd8ujWT4V9GP3Hc/I3F5tUKxPWiVEKTVRr6wzjwtXctOnhcbB3FeRtGDY
+CfVBYMSEDJmAyMchfST/GwdG46Ak2TSaMpOf6tL5aMw+xfmDI68JGwG0LNliyEoW
+xOHRCtWd5Q+Sn3rgx4h6nzdZOGHw3HwDbsX/y/dZNc7luUImEWwTyhohnO9XqaBX
+EsdMDJmBaoVum+sR6ch08TsqrTHAfdB3xJF37Wc=
+-----END CERTIFICATE-----`;
+describe("Cert Utilities Test", () => {
+ it("oid to name", () => {
+ expect(oidToName("thisisnotanoid")).toBe("");
+
expect(oidToName("1.2.840.113549.1.1.12")).toBe("sha384WithRSAEncryption");
+ });
+
+ it("sha cert digest", () => {
+ const cert = forge.pki.certificateFromPem(certPEM);
+
expect(pkiCertToSHA1(cert)).toBe("4afd0f20041efe4474ef44a125d0715cacc269e5");
+
expect(pkiCertToSHA256(cert)).toBe("531d5eca87cc038077c6403615e0df646c0eac604a6292b79db1f8a014a7fdf8");
+
+ expect(() =>
pkiCertToSHA1(forge.pki.createCertificate())).toThrowError();
+ expect(() =>
pkiCertToSHA256(forge.pki.createCertificate())).toThrowError();
+ });
+});
diff --git a/experimental/traffic-portal/src/app/core/certs/cert.util.ts
b/experimental/traffic-portal/src/app/core/certs/cert.util.ts
new file mode 100644
index 0000000000..0f6a3e610a
--- /dev/null
+++ b/experimental/traffic-portal/src/app/core/certs/cert.util.ts
@@ -0,0 +1,51 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { pki, md, asn1 } from "node-forge";
+
+/**
+ * Converts a given oid to it's human-readable name
+ *
+ * @param oid The oid to translate
+ * @returns The human-readable oid
+ */
+export function oidToName(oid: string): string {
+ if (oid in pki.oids) {
+ return pki.oids[oid];
+ }
+ return "";
+}
+
+/**
+ * Calculate the SHA-1 of a given certificate
+ *
+ * @param cert The certificate to hash
+ * @returns SHA-1 of the cert
+ */
+export function pkiCertToSHA1(cert: pki.Certificate): string {
+ const md1 = md.sha1.create();
+ md1.update(asn1.toDer(pki.certificateToAsn1(cert)).getBytes());
+ return md1.digest().toHex();
+}
+
+/**
+ * Calculate the SHA-256 of a given certificate
+ *
+ * @param cert The certificate to hash
+ * @returns SHA-256 of the cert
+ */
+export function pkiCertToSHA256(cert: pki.Certificate): string {
+ const md256 = md.sha256.create();
+ md256.update(asn1.toDer(pki.certificateToAsn1(cert)).getBytes());
+ return md256.digest().toHex();
+}
diff --git a/experimental/traffic-portal/src/app/core/certs/certs.module.ts
b/experimental/traffic-portal/src/app/core/certs/certs.module.ts
new file mode 100644
index 0000000000..0a62b029f9
--- /dev/null
+++ b/experimental/traffic-portal/src/app/core/certs/certs.module.ts
@@ -0,0 +1,49 @@
+/*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+import { CommonModule } from "@angular/common";
+import { NgModule } from "@angular/core";
+import { RouterModule, Routes } from "@angular/router";
+
+import { AppUIModule } from "src/app/app.ui.module";
+import { CertViewerComponent } from
"src/app/core/certs/cert-viewer/cert-viewer.component";
+import { SharedModule } from "src/app/shared/shared.module";
+
+import { CertAuthorComponent } from "./cert-author/cert-author.component";
+import { CertDetailComponent } from "./cert-detail/cert-detail.component";
+
+export const ROUTES: Routes = [
+ {component: CertViewerComponent, path: "ssl/:xmlId"},
+ {component: CertViewerComponent, path: "ssl"}
+];
+
+/**
+ * Declares the module for SSL certificates. Is seperated since `node-forge`
which provides
+ * SSL functions is quite large.
+ */
+@NgModule({
+ declarations: [
+ CertViewerComponent,
+ CertDetailComponent,
+ CertAuthorComponent,
+ ],
+ exports: [],
+ imports: [
+ CommonModule,
+ AppUIModule,
+ SharedModule,
+ RouterModule.forChild(ROUTES)
+ ]
+})
+export class CertsModule {
+}
diff --git a/experimental/traffic-portal/src/app/core/core.module.ts
b/experimental/traffic-portal/src/app/core/core.module.ts
index fa0757870c..db9257a8c5 100644
--- a/experimental/traffic-portal/src/app/core/core.module.ts
+++ b/experimental/traffic-portal/src/app/core/core.module.ts
@@ -17,9 +17,11 @@
* limitations under the License.
*/
import { CommonModule } from "@angular/common";
-import { NgModule } from "@angular/core";
+import { NgModule, Type } from "@angular/core";
import { RouterModule, type Routes } from "@angular/router";
+import { type CertsModule } from "src/app/core/certs/certs.module";
+
import { AppUIModule } from "../app.ui.module";
import { AuthenticatedGuard } from "../guards/authenticated-guard.service";
import { SharedModule } from "../shared/shared.module";
@@ -73,6 +75,14 @@ import { UserRegistrationDialogComponent } from
"./users/user-registration-dialo
import { UsersComponent } from "./users/users.component";
export const ROUTES: Routes = [
+ {
+ children: [{
+ loadChildren: async (): Promise<Type<CertsModule>> =>
import("./certs/certs.module")
+ .then(mod => mod.CertsModule),
+ path: ""
+ }],
+ path: "certs"
+ },
{ component: DashboardComponent, path: "" },
{ component: AsnDetailComponent, path: "asns/:id"},
{ component: AsnsTableComponent, path: "asns" },
diff --git
a/experimental/traffic-portal/src/app/core/statuses/statuses-table/statuses-table.component.html
b/experimental/traffic-portal/src/app/core/statuses/statuses-table/statuses-table.component.html
index b038758362..ff8f9140f6 100644
---
a/experimental/traffic-portal/src/app/core/statuses/statuses-table/statuses-table.component.html
+++
b/experimental/traffic-portal/src/app/core/statuses/statuses-table/statuses-table.component.html
@@ -11,7 +11,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
-<mat-card class="page-content" appearance="outlined">
+<mat-card class="page-content">
<div class="search-container">
<input type="search" name="fuzzControl" aria-label="Search
Statuses" inputmode="search" role="search" accesskey="/"
placeholder="Fuzzy Search"
[formControl]="fuzzControl" (input)="updateURL()"/>
diff --git
a/experimental/traffic-portal/src/app/core/statuses/statuses-table/statuses-table.component.ts
b/experimental/traffic-portal/src/app/core/statuses/statuses-table/statuses-table.component.ts
index e97879d215..b8d7c179a5 100644
---
a/experimental/traffic-portal/src/app/core/statuses/statuses-table/statuses-table.component.ts
+++
b/experimental/traffic-portal/src/app/core/statuses/statuses-table/statuses-table.component.ts
@@ -92,12 +92,6 @@ export class StatusesTableComponent implements OnInit {
/** Form controller for the user search input. */
public fuzzControl = new FormControl<string>("", {nonNullable: true});
- /**
- * Constructs the component with its required injections.
- *
- * @param api The Servers API which is used to provide row data.
- * @param navSvc Manages the header
- */
constructor(
private readonly dialog: MatDialog,
private readonly route: ActivatedRoute,
diff --git
a/experimental/traffic-portal/src/app/shared/navigation/navigation.service.ts
b/experimental/traffic-portal/src/app/shared/navigation/navigation.service.ts
index 3130756f97..fc762b7069 100644
---
a/experimental/traffic-portal/src/app/shared/navigation/navigation.service.ts
+++
b/experimental/traffic-portal/src/app/shared/navigation/navigation.service.ts
@@ -230,6 +230,10 @@ export class NavigationService {
href: "/core/iso-gen",
name: "Generate System ISO"
},
+ {
+ href: "/core/certs/ssl",
+ name: "Inspect Certificate"
+ },
{
href: `${this.tpv1Url}/jobs`,
name: "Invalidate Content"