Files
b210-k7-fpga/lib/sec/license_check.sv
T
Martin Braun cc28ffff60 fpga: Add license checker module
Original-commit: 8ed8927b944e7ad8a5da040c7f1951aad3535e11
2024-05-27 17:05:06 +02:00

267 lines
10 KiB
Systemverilog

//
// 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