fpga: Add license checker module

Original-commit: 8ed8927b944e7ad8a5da040c7f1951aad3535e11
This commit is contained in:
Martin Braun
2024-05-27 17:05:06 +02:00
committed by joergho
parent cf85a6d40c
commit cc28ffff60
5 changed files with 642 additions and 0 deletions
+1
View File
@@ -13,4 +13,5 @@ sha256_core.v \
sha256_k_constants.v \
sha256_stream.v \
sha256_w_mem.v \
license_check.sv \
))
+266
View File
@@ -0,0 +1,266 @@
//
// Copyright 2023 Ettus Research, a National Instruments Brand
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Module: license_check
//
// Description:
//
// License checker for RFNoC blocks, using a combination of private key, device
// serial, and feature flag. This module can be used to load a license key.
// If the key is valid, it will assert an output signal, which can be consumed
// by the module for which the license key is meant.
//
// The license key is a combination of a feature identifier (32-bit) and a
// SHA256 hash (256 bits). In total, the license key is thus 36 bytes long.
//
// The hash is calculated from:
// - A device serial
// - A private key
// - The 32-bit feature identifier
//
// Usage:
// - Upon instantiation, make sure that the serial input is connected to this
// device's serial number.
// - To unlock a feature, first write the feature ID to BASE_ADDR. This will
// calculate a SHA256 hash.
// - Then, write the rest of the user key in 32-bit words to BASE_ADDR+4. This
// module will compare the user key with the internally generated hash bit
// by bit.
// - After all 8 32-bit words have been written, the feature enable line will
// be asserted if the uploaded key matched the SHA256 hash.
//
// Note that in this implementation, PKEY_W + SERIAL_W must be less than
// 512-32-64-8 as we require 32 bits for the feature flag, and 8 bits padding
// (as well as the 64-bit message length).
//
// Registers (Relative to BASE_ADDR):
// - FID_ADDR (BASE_ADDR): Feature ID (w)
// - KEY_ADDR (BASE_ADDR+4): User key, one 32-bit word at a time (w)
// - RB_ADDR (BASE_ADDR+8): Readback, will provide info about the last feature
// ID that was written. Bit 31 tells us if the feature
// was unlocked. The rest tells us which feature index
// it is.
//
// Parameters:
//
// BASE_ADDR: Address for writing the feature flag. Key values are written to
// BASE_ADDR+4 (see above).
// PKEY_W: Width of private key (in bits).
// SERIAL_W: Width of serial (in bits). See note above on available lengths.
// NUM_FEATURES: Number of features that can be unlocked
// FEATURE_IDS: A list of 32-bit values that identify features
// PRIVATE_KEY: The private key
// DEVICE_TYPE: For now, can only be ULTRASCALE.
//
`default_nettype none
module license_check #(
parameter BASE_ADDR = 0,
parameter PKEY_W = 312,
parameter SERIAL_W = 96,
parameter NUM_FEATURES = 1,
parameter [(32*NUM_FEATURES)-1:0] FEATURE_IDS = {32'hC0DE},
parameter [PKEY_W-1:0] PRIVATE_KEY = 0,
parameter DEVICE_TYPE = "ULTRASCALE"
)(
input wire clk,
input wire rst,
input wire [SERIAL_W-1:0] serial,
input wire s_ctrlport_req_wr,
input wire s_ctrlport_req_rd,
input wire [19:0] s_ctrlport_req_addr,
input wire [31:0] s_ctrlport_req_data,
output wire s_ctrlport_resp_ack,
output wire [ 1:0] s_ctrlport_resp_status,
output wire [31:0] s_ctrlport_resp_data,
output reg [NUM_FEATURES-1:0] feature_enabled = 0
);
`include "../rfnoc/core/ctrlport.vh"
//! Number of bits in the SHA256 message that encode the message length
// (see SHA256 standard for details)
localparam MSGLEN_W = 64;
localparam FID_W = 32; // Number of bits in feature ID
// Number of bits that are being SHA-hashed
localparam [MSGLEN_W-1:0] MSGLEN = PKEY_W + SERIAL_W + FID_W;
// We have 512 bits in a SHA-input block. The remaining bits that are not part
// of the message are the padding and the message length.
localparam PADDING_W = 512 - MSGLEN - MSGLEN_W; // Number of padding bits
// We pad at least with a full byte. This wastes 7 bits of potential private
// key length, but it also means we can't accidentally load invalid private
// keys.
if (PADDING_W < 8) begin : gen_assertion
ERROR_max_key_len_exceeded();
end
// Padding bits (10000...)
localparam [PADDING_W-1:0] PADDING = {1'b1, {(PADDING_W-1){1'b0}}};
// Length of the feature indexer reg
localparam FIDXREG_W = $clog2(NUM_FEATURES)+1;
// A value that is not a feature index
localparam NO_FID = {FIDXREG_W{1'b1}};
// Valid addresses on the CtrlPort bus
localparam FID_ADDR = BASE_ADDR;
localparam KEY_ADDR = BASE_ADDR + 4;
localparam RB_ADDR = BASE_ADDR + 8;
// States
typedef enum logic [2:0] {
ST_IDLE,
ST_CALC,
ST_USERKEYDONE,
ST_ACK,
ST_CTRLPORT_ERROR
} state_t;
// Registers & Wires
state_t state = ST_IDLE;
reg key_valid = 1'b0; // Tracks if current hash matches
reg fid_hashed = 1'b0; // Tracks if we have hashed a feature ID
reg [2:0] word_cnt = 3'd7; // Track which word we are currently comparing
// The index of the feature ID we're hashing/comparing
reg [FIDXREG_W-1:0] feature_idx = NO_FID;
wire fid_wr_req = (s_ctrlport_req_wr && (s_ctrlport_req_addr == FID_ADDR));
wire key_wr_req = (s_ctrlport_req_wr && (s_ctrlport_req_addr == KEY_ADDR));
wire [255:0] sha_digest;
wire sha_digest_valid;
wire sha_tready;
wire sha_tvalid = fid_wr_req && (state == ST_IDLE);
integer feat_i;
wire [511:0] sha_input = {
serial,
PRIVATE_KEY,
s_ctrlport_req_data,
PADDING,
MSGLEN
};
// Actual SHA256 calculation module
sha256_stream sha256_inst (
.clk (clk),
.rst (rst),
.mode (1'b1), // Always SHA256, not SHA224
.s_tdata_i (sha_input),
.s_tlast_i (1'b1), // All SHA256 transactions in this module are single-block
.s_tvalid_i (sha_tvalid),
.s_tready_o (sha_tready),
.digest_o (sha_digest),
.digest_valid_o (sha_digest_valid)
);
// State machine
always @(posedge clk) begin
if (rst) begin
state <= ST_IDLE;
word_cnt <= 3'd7;
key_valid <= 1'b0;
fid_hashed <= 1'b0;
// On reset, all features are disabled. Otherwise, they always stay on.
feature_enabled <= {NUM_FEATURES{1'b0}};
feature_idx <= NO_FID;
end else case (state)
ST_IDLE : begin
//// Idle state: We're waiting on input via CtrlPort.
// - If we get a feature flag to hash, we store it and start hashin'
// Note that the sha256 module is connected straight to the input and
// will start calculating when this condition is met.
if (fid_wr_req) begin
// If we switched to ST_CALC before sha_tready is asserted, then we
// would read an invalid hash from the sha256 module. However, we wait
// with returning an ACK when the sha256 module is busy, so we can
// assume the hashing module is always ready in this scenario.
// synthesis translate_off
assert(sha_tready);
// synthesis translate_on
state <= ST_CALC;
word_cnt <= 3'd7;
key_valid <= 1'b0;
fid_hashed <= 1'b0;
// Reset the feature index to a non-feature-index value
feature_idx <= NO_FID;
// We store which feature ID we've been comparing
for (feat_i = 0; feat_i < NUM_FEATURES; feat_i = feat_i + 1) begin
if (s_ctrlport_req_data == FEATURE_IDS[(32*feat_i) +: 32]) begin
feature_idx <= feat_i;
end
end
// - If we get a key to compare, but we didn't previously hash a feature
// ID, then we return an error code, because we can't compare it with
// anything yet!
end else if (key_wr_req && !fid_hashed) begin
state <= ST_CTRLPORT_ERROR;
// - If we get a key to compare, and we *did* previously hash a flag, we
// start comparing word-for-word. At any point, if the comparison fails,
// our key becomes and stays invalid.
end else if (key_wr_req) begin
key_valid <= (word_cnt == 7 ? (sha_digest[255:224] == s_ctrlport_req_data) :
word_cnt == 6 ? (sha_digest[223:192] == s_ctrlport_req_data) :
word_cnt == 5 ? (sha_digest[191:160] == s_ctrlport_req_data) :
word_cnt == 4 ? (sha_digest[159:128] == s_ctrlport_req_data) :
word_cnt == 3 ? (sha_digest[127: 96] == s_ctrlport_req_data) :
word_cnt == 2 ? (sha_digest[ 95: 64] == s_ctrlport_req_data) :
word_cnt == 1 ? (sha_digest[ 63: 32] == s_ctrlport_req_data) :
(sha_digest[ 31: 0] == s_ctrlport_req_data))
&& (word_cnt == 7 || key_valid);
word_cnt <= word_cnt - 1;
state <= word_cnt == 0 ? ST_USERKEYDONE : ST_ACK;
// Handle readback
end else if (s_ctrlport_req_rd && (s_ctrlport_req_addr == RB_ADDR)) begin
state <= ST_ACK;
// Any other read/write request is ignored
end else begin
state <= ST_IDLE;
end
end // ST_IDLE
ST_CALC : begin
//// Hash calculation state:
// We wait here until the SHA is calculated. We don't accept CtrlPort
// transactions until then, either. Once the feature flag has been hashed,
// we return to idle state and wait for a user key for comparison.
fid_hashed <= sha_digest_valid;
state <= sha_digest_valid ? ST_IDLE : ST_CALC;
end
ST_USERKEYDONE : begin
//// User-key-done state: We get here when 8 words have been provided for
// comparison. If an invalid key is uploaded for a feature after it was
// already unlocked, we leave the flag enabled.
feature_enabled[feature_idx] <= key_valid | feature_enabled[feature_idx];
state <= ST_ACK;
end
ST_ACK, ST_CTRLPORT_ERROR : state <= ST_IDLE;
default : state <= ST_IDLE;
endcase
end // always @(posedge clk)
assign s_ctrlport_resp_ack = (state == ST_CALC && sha_digest_valid) ||
(state == ST_ACK) ||
(state == ST_CTRLPORT_ERROR);
assign s_ctrlport_resp_status =
(state == ST_CTRLPORT_ERROR) ? CTRL_STS_CMDERR : CTRL_STS_OKAY;
assign s_ctrlport_resp_data = (feature_idx == NO_FID)
? 32'b0
: {feature_enabled[feature_idx], {(31-FIDXREG_W){1'b0}}, feature_idx};
endmodule
`default_nettype wire
+45
View File
@@ -0,0 +1,45 @@
#
# Copyright 2023 Ettus Research, a National Instruments Brand
#
# SPDX-License-Identifier: LGPL-3.0-or-later
#
#-------------------------------------------------
# Top-of-Makefile
#-------------------------------------------------
# Define BASE_DIR to point to the "top" dir
BASE_DIR = $(abspath ../../../../top)
# Include viv_sim_preamble after defining BASE_DIR
include $(BASE_DIR)/../tools/make/viv_sim_preamble.mak
#-------------------------------------------------
# Design Specific
#-------------------------------------------------
# Include makefiles and sources for the DUT and its dependencies
DESIGN_SRCS += \
$(abspath ../../../sec/license_check.sv) \
$(abspath ../../../sec/sha256.v) \
$(abspath ../../../sec/sha256_core.v) \
$(abspath ../../../sec/sha256_k_constants.v) \
$(abspath ../../../sec/sha256_stream.v) \
$(abspath ../../../sec/sha256_w_mem.v) \
$(VIVADO_PATH)/data/verilog/src/glbl.v \
MODELSIM_ARGS += glbl
#-------------------------------------------------
# Testbench Specific
#-------------------------------------------------
SIM_TOP = license_check_tb
SIM_SRCS = \
$(abspath license_check_tb.sv) \
#-------------------------------------------------
# Bottom-of-Makefile
#-------------------------------------------------
# Include all simulator specific makefiles here
# Each should define a unique target to simulate
# e.g. xsim, vsim, etc and a common "clean" target
include $(BASE_DIR)/../tools/make/viv_simulator.mak
+38
View File
@@ -0,0 +1,38 @@
#
# Copyright 2023 Ettus Research, a National Instruments Brand
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
"""
Calculate hash values for license_check_tb
"""
import hashlib
serial = [0x01, 0x23, 0x45, 0x67, 0x8A, 0xBC, 0xDE, 0xF0, 0x0C, 0x0D, 0xE0, 0xBB]
pkey = [0x00] * 37 + [0x05, 0xEC]
feature0 = [0x00, 0x00, 0xC0, 0xDE]
feature1 = [0x00, 0x00, 0xF0, 0x0D]
def main():
"""
Calc and print hashes
"""
sha0 = hashlib.sha256()
sha0.update(bytes(serial + pkey + feature0))
hash0 = sha0.hexdigest()
sha1 = hashlib.sha256()
sha1.update(bytes(serial + pkey + feature1))
hash1 = sha1.hexdigest()
print(f"// Feature 0 hash: 256'h{hash0}")
hash0_words = [f"hash0_{int(i/8)} = 32'h" + hash0[i:i+8] for i in range(0, 64, 8)]
print("// Individual words:")
print(";\n localparam ".join(hash0_words))
print(f"// Feature 1 hash: 256'h{hash1}")
hash1_words = [f"hash1_{int(i/8)} = 32'h" + hash1[i:i+8] for i in range(0, 64, 8)]
print("// Individual words:")
print(";\n localparam ".join(hash1_words))
if __name__ == "__main__":
main()
@@ -0,0 +1,292 @@
//
// Copyright 2023 Ettus Research, a National Instruments Brand
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Description:
//
// Testbench for license_check and the sha256 module
//
`default_nettype none
module license_check_tb ();
// Include macros and time declarations for use with PkgTestExec
`include "test_exec.svh"
import PkgTestExec::*;
localparam real CLK_PERIOD = 10.0; // ns
localparam [95:0] SERIAL = 96'h012345678ABCDEF00C0DE0BB;
localparam int PKEY_W = 312;
localparam [PKEY_W-1:0] PRIVATE_KEY = 312'h5EC;
localparam int FEATURE0 = 32'hC0DE;
localparam int FEATURE1 = 32'hF00D;
localparam FEATURE_ADDR = 0;
localparam KEY_ADDR = 4;
localparam RB_ADDR = 8;
// Note: hashes are calculated with gen_sha256.py
// Feature 0 hash: 256'h80f122aad504a1ec7ae8f37b4eee414cf1cf32ecc543ee2e1d4e1d4ae59eb41b
// Individual words:
localparam hash0_0 = 32'h80f122aa;
localparam hash0_1 = 32'hd504a1ec;
localparam hash0_2 = 32'h7ae8f37b;
localparam hash0_3 = 32'h4eee414c;
localparam hash0_4 = 32'hf1cf32ec;
localparam hash0_5 = 32'hc543ee2e;
localparam hash0_6 = 32'h1d4e1d4a;
localparam hash0_7 = 32'he59eb41b;
// Feature 1 hash: 256'hefd81f78a5fdee0a16ed039cfa51db769a8da088328f8ed73d6af85ba1ce57d4
// Individual words:
localparam hash1_0 = 32'hefd81f78;
localparam hash1_1 = 32'ha5fdee0a;
localparam hash1_2 = 32'h16ed039c;
localparam hash1_3 = 32'hfa51db76;
localparam hash1_4 = 32'h9a8da088;
localparam hash1_5 = 32'h328f8ed7;
localparam hash1_6 = 32'h3d6af85b;
localparam hash1_7 = 32'ha1ce57d4;
//---------------------------------------------------------------------------
// Clocks and Resets
//---------------------------------------------------------------------------
bit clk;
bit rst;
bit rst_dut = 0;
sim_clock_gen #(.PERIOD(CLK_PERIOD))
clk_gen (.clk(clk), .rst(rst));
//---------------------------------------------------------------------------
// Device Under Test (DUT)
//---------------------------------------------------------------------------
logic i_ctrlport_req_rd = 1'b0;
logic i_ctrlport_req_wr = 1'b0;
logic [19:0] i_ctrlport_req_addr = 20'h0;
logic [31:0] i_ctrlport_req_data = 32'h0;
logic o_ctrlport_resp_ack;
logic [1:0] o_ctrlport_resp_status;
logic [31:0] o_ctrlport_resp_data;
logic [1:0] feature_enabled;
license_check #(
.BASE_ADDR (0),
.PKEY_W (PKEY_W),
.SERIAL_W (96),
.NUM_FEATURES (2),
.FEATURE_IDS ({FEATURE1, FEATURE0}),
.PRIVATE_KEY (PRIVATE_KEY)
) license_check_dut (
.clk(clk),
.rst(rst | rst_dut),
.serial(SERIAL),
.s_ctrlport_req_wr (i_ctrlport_req_wr),
.s_ctrlport_req_rd (i_ctrlport_req_rd),
.s_ctrlport_req_addr (i_ctrlport_req_addr),
.s_ctrlport_req_data (i_ctrlport_req_data),
.s_ctrlport_resp_ack (o_ctrlport_resp_ack),
.s_ctrlport_resp_status (o_ctrlport_resp_status),
.s_ctrlport_resp_data (o_ctrlport_resp_data),
.feature_enabled(feature_enabled)
);
//--------------------------------
// Tasks
//--------------------------------
// Write a data word via ctrlport transaction. Will run into timeout if no
// ACK is returned.
task automatic ctrlport_poke32(int addr, int data);
i_ctrlport_req_addr <= addr;
i_ctrlport_req_data <= data;
i_ctrlport_req_wr <= 1'b1;
@(posedge clk);
i_ctrlport_req_wr <= 1'b0;
while (!o_ctrlport_resp_ack) @(posedge clk);
test.assert_error(
!o_ctrlport_resp_status,
$sformatf("CtrlPort write response status not zero (%d)!",
o_ctrlport_resp_status));
endtask
task automatic ctrlport_poke32_status(int addr, int data, int exp_status);
i_ctrlport_req_addr <= addr;
i_ctrlport_req_data <= data;
i_ctrlport_req_wr <= 1'b1;
@(posedge clk);
i_ctrlport_req_wr <= 1'b0;
while (!o_ctrlport_resp_ack) @(posedge clk);
test.assert_error(
o_ctrlport_resp_status == exp_status,
$sformatf("CtrlPort write response status not %d (%d)!",
exp_status, o_ctrlport_resp_status));
endtask
task automatic ctrlport_peek32(int addr, int expected_data);
i_ctrlport_req_addr <= addr;
i_ctrlport_req_rd <= 1'b1;
@(posedge clk);
i_ctrlport_req_rd <= 1'b0;
while (!o_ctrlport_resp_ack) @(posedge clk);
test.assert_error(
!o_ctrlport_resp_status,
$sformatf("CtrlPort read response status not zero (%d)!",
o_ctrlport_resp_status));
test.assert_error(
o_ctrlport_resp_data == expected_data,
$sformatf("Incorrect peek value: %x! Expected %x.",
o_ctrlport_resp_data, expected_data));
endtask
//---------------------------------------------------------------------------
// Main Test Process
//---------------------------------------------------------------------------
initial begin : tb_main
string tb_name;
tb_name = $sformatf("device_dna_ctrlport");
test.start_tb(tb_name, 10ms);
//-------------------------------------------------------
// Reset clock generator and wait for reset to complete
//-------------------------------------------------------
test.start_test("Reset", 100us);
clk_gen.reset();
if (rst) @rst;
test.end_test();
//--------------------------------
// Test Sequences
//--------------------------------
// Verify that all features are disabled after reset
test.start_test("Checking reset state", 2us);
test.assert_error(
feature_enabled == 0,
$sformatf("Features enabled after reset: %x", feature_enabled));
ctrlport_peek32(RB_ADDR, 32'b00000000000000000000000000000000);
test.end_test();
/// Regular feature unlock
test.start_test("Checking feature 0 unlock", 100us);
ctrlport_poke32(FEATURE_ADDR, FEATURE0);
ctrlport_poke32(KEY_ADDR, hash0_0);
ctrlport_poke32(KEY_ADDR, hash0_1);
ctrlport_poke32(KEY_ADDR, hash0_2);
ctrlport_poke32(KEY_ADDR, hash0_3);
ctrlport_poke32(KEY_ADDR, hash0_4);
ctrlport_poke32(KEY_ADDR, hash0_5);
ctrlport_poke32(KEY_ADDR, hash0_6);
ctrlport_poke32(KEY_ADDR, hash0_7);
test.assert_error(feature_enabled == 2'b1,
$sformatf("Feature enable is not 1 (%x)", feature_enabled));
ctrlport_peek32(RB_ADDR, 32'b10000000000000000000000000000000);
test.end_test();
test.start_test("Checking feature 1 unlock", 100us);
ctrlport_poke32(FEATURE_ADDR, FEATURE1);
ctrlport_poke32(KEY_ADDR, hash1_0);
ctrlport_poke32(KEY_ADDR, hash1_1);
ctrlport_poke32(KEY_ADDR, hash1_2);
ctrlport_poke32(KEY_ADDR, hash1_3);
ctrlport_poke32(KEY_ADDR, hash1_4);
ctrlport_poke32(KEY_ADDR, hash1_5);
ctrlport_poke32(KEY_ADDR, hash1_6);
ctrlport_poke32(KEY_ADDR, hash1_7);
test.assert_error(feature_enabled == 2'b11,
$sformatf("Feature enable is not 0b11 (%x)", feature_enabled));
ctrlport_peek32(RB_ADDR, 32'b10000000000000000000000000000001);
test.end_test();
/// Reset and make sure features are no longer enabled
test.start_test("Testing reset after unlock", 100us);
rst_dut <= 1;
@(posedge clk);
rst_dut <= 0;
@(posedge clk);
test.assert_error(feature_enabled == 2'b00,
$sformatf("Feature enable is not 0b00 (%x)", feature_enabled));
ctrlport_peek32(RB_ADDR, 32'h0);
test.end_test();
/// Writing a hash value now is invalid
test.start_test("Checking error case: No FID loaded", 100us);
ctrlport_poke32_status(KEY_ADDR, 32'h1234ABCD, 2'b1);
test.end_test();
/// Try uploading invalid key
test.start_test("Checking feature 0 unlock with invalid key", 100us);
ctrlport_poke32(FEATURE_ADDR, FEATURE0);
ctrlport_poke32(KEY_ADDR, hash0_0);
ctrlport_poke32(KEY_ADDR, hash0_1);
ctrlport_poke32(KEY_ADDR, hash0_2);
ctrlport_poke32(KEY_ADDR, hash0_3);
ctrlport_poke32(KEY_ADDR, hash0_4);
ctrlport_poke32(KEY_ADDR, hash0_5);
ctrlport_poke32(KEY_ADDR, hash0_6);
ctrlport_poke32(KEY_ADDR, 32'h0);
test.assert_error(feature_enabled == 2'b0,
$sformatf("Feature enable is not 0 (%x)", feature_enabled));
test.end_test();
/// Now again valid key after failure
test.start_test("Checking feature 0 unlock with valid key again after fail", 100us);
ctrlport_poke32(FEATURE_ADDR, FEATURE0);
ctrlport_poke32(KEY_ADDR, hash0_0);
ctrlport_poke32(KEY_ADDR, hash0_1);
ctrlport_poke32(KEY_ADDR, hash0_2);
ctrlport_poke32(KEY_ADDR, hash0_3);
ctrlport_poke32(KEY_ADDR, hash0_4);
ctrlport_poke32(KEY_ADDR, hash0_5);
ctrlport_poke32(KEY_ADDR, hash0_6);
ctrlport_poke32(KEY_ADDR, hash0_7);
test.assert_error(feature_enabled == 2'b1,
$sformatf("Feature enable is not 0b1 (%x)", feature_enabled));
test.end_test();
/// Upload part of feature 0, interrupt, then feature 1
test.start_test("Checking feature 1 unlock after reset", 100us);
ctrlport_poke32(FEATURE_ADDR, FEATURE0);
ctrlport_poke32(KEY_ADDR, hash0_0);
ctrlport_poke32(KEY_ADDR, hash0_1);
ctrlport_poke32(KEY_ADDR, hash0_2);
ctrlport_poke32(FEATURE_ADDR, FEATURE1);
ctrlport_poke32(KEY_ADDR, hash1_0);
ctrlport_poke32(KEY_ADDR, hash1_1);
ctrlport_poke32(KEY_ADDR, hash1_2);
ctrlport_poke32(KEY_ADDR, hash1_3);
ctrlport_poke32(KEY_ADDR, hash1_4);
ctrlport_poke32(KEY_ADDR, hash1_5);
ctrlport_poke32(KEY_ADDR, hash1_6);
ctrlport_poke32(KEY_ADDR, hash1_7);
test.assert_error(feature_enabled == 2'b11,
$sformatf("Feature enable is not 0b11 (%x)", feature_enabled));
ctrlport_peek32(RB_ADDR, 32'b10000000000000000000000000000001);
test.end_test();
//--------------------------------
// Finish Up
//--------------------------------
test.end_tb();
end : tb_main
endmodule : license_check_tb
`default_nettype wire