Tokeneer Fully Verified with SPARK 2014
by Yannick Moy –
Tokeneer is a software for controlling physical access to a secure enclave by means of a fingerprint sensor. This software was created by Altran (Praxis at the time) in 2003 using the previous generation of SPARK language and tools, as part of a project commissioned by the NSA to investigate the rigorous development of critical software using formal methods.
The project artefacts, including the source code, were released as open source in 2008. Tokeneer was widely recognized as a milestone in industrial formal verification. Original project artefacts, including the original source code in SPARK 2005, are available here.
We recently transitioned this software to SPARK 2014, and it allowed us to go beyond what was possible with the previous SPARK technology. The initial transition by Altran and AdaCore took place in 2013-2014, when we translated all the contracts from SPARK 2005 syntax (stylized comments in the code) to SPARK 2014 syntax (aspects in the code). But at the time we did not invest the time to fully prove the resulting translated code. This is what we have now completed. The resulting code is available on GitHub. It will also be available in future SPARK releases as one of the distributed examples.
What we did
With a few changes, we went from 234 unproved checks on Tokeneer code (the version originally translated to SPARK 2014), down to 39 unproved but justified checks. The justification is important here: there are limitations to GNATprove analysis, so it is expected that users must sometimes step in and take responsibility for unproved checks.
Using predicates to express constraints
Most of the 39 justifications in Tokeneer code are for string concatenations that involve attribute 'Image. GNATprove currently does not know that S'Image(X), for a scalar type S and a variable X of this type, returns a rather small string (as specified in Ada RM), so it issues a possible range check message when concatenating such an image with any other string. We chose to isolate such calls to 'Image in dedicated functions, with suitable predicates on their return type to convey the information about the small string result. Take for example enumeration type ElementT in audittypes.ads. We define a function ElementT_Image which returns a small string starting at 1 and with length less than 20 as follows:
function ElementT_Image (X : ElementT) return CommonTypes.StringF1L20 is
(ElementT'Image (X));
pragma Annotate (GNATprove, False_Positive,
"range check might fail",
"Image of enums of type ElementT are short strings starting at index 1");
pragma Annotate (GNATprove, False_Positive,
"predicate check might fail",
"Image of enums of type ElementT are short strings starting at index 1");
Note the use of pragma Annotate to justify the range check message and the predicate check message that are generated by GNATprove otherwise. Type StringF1L20 is defined as a subtype of the standard String type with additional constraints expressed as predicates. In fact, we create an intermediate subtype StringF1 of strings that start at index 1 and which are not "super flat", i.e. their last index is at least 0. StringF1L20 inherits from the predicate of StringF1 and adds the constraint that the length of the string is no more than 20:
subtype StringF1 is String with
Predicate => StringF1'First = 1 and StringF1'Last >= 0;
subtype StringF1L20 is StringF1 with
Predicate => StringF1L20'Last <= 20;
Moving query functions to the spec
Another crucial change was to give visibility to client code over query functions used in contracts. Take for example the API in admin.ads. It defines the behavior of the administrator through subprograms whose contracts use query functions RolePresent, IsPresent and IsDoingOp:
procedure Logout (TheAdmin : out T)
with Global => null,
Post => not IsPresent (TheAdmin)
and not IsDoingOp (TheAdmin);
The issue was that these query functions, while conveniently abstracting away the details of what it means for the administrator to be present, or to be doing an operation, were defined in the body of package Admin, inside file admin.adb. As a result, the proof of client code of Admin had to consider these calls as blackboxes, which resulted in many unprovable checks. The fix here consisted in moving the definition for the query functions inside the private part of the spec file admin.ads: this way, client code still does not see their implementation, but GNATprove can use these expression functions in proving client code.
function RolePresent (TheAdmin : T) return PrivTypes.PrivilegeT is
(TheAdmin.RolePresent);
function IsPresent (TheAdmin : T) return Boolean is
(TheAdmin.RolePresent in PrivTypes.AdminPrivilegeT);
function IsDoingOp (TheAdmin : T) return Boolean is
(TheAdmin.CurrentOp in OpT);
Using type invariants to enforce global invariants
Some global properties on the version in SPARK 2005 were justified manually, like the global invariant maintained in package Auditlog over the global variables encoding the state of the files used to log operations: CurrentLogFile, NumberLogEntries, UsedLogFiles, LogFileEntries. Here is the text for this justification:
-- Proof Review file for
-- procedure AuditLog.AddElementToLog
-- VC 6
-- C1: fld_numberlogentries(state) = (fld_length(fld_usedlogfiles(state)) - 1)
-- * 1024 + element(fld_logfileentries(state), [fld_currentlogfile(state)
-- ]) .
-- C1 is a package state invariant.
-- proof shows that all public routines that modify NumberLogEntries, UsedLogFiles.Length,
-- CurrentLogFile or LogFileEntries(CurrentLogFile) maintain this invariant.
-- This invariant has not been propogated to the specification since it would unecessarily
-- complicate proof of compenents that use the facilities from this package.
We can do better in SPARK 2014, by expressing this property as a type invariant. This requires all four variables to become components of the same record type, so that a single global variable LogFiles replaces them:
type LogFileStateT is record
CurrentLogFile : LogFileIndexT := 1;
NumberLogEntries : LogEntryCountT := 0;
UsedLogFiles : LogFileListT :=
LogFileListT'(List => (others => 1),
Head => 1,
LastI => 1,
Length => 1);
LogFileEntries : LogFileEntryT := (others => 0);
end record
with Type_Invariant =>
Valid_NumberLogEntries
(CurrentLogFile, NumberLogEntries, UsedLogFiles, LogFileEntries);
LogFiles : LogFilesT := LogFilesT'(others => File.NullFile)
with Part_Of => FileState;
With this change, all public subprograms updating the state of log files can now assume the invariant holds on entry (it is checked by GNATprove on every call) and must restore it on exit (it is checked by GNATprove when returning from the subprogram). Locally defined subprograms need not obey this constraint however, which is exactly what is needed here. One subtlety is that some of these local subprograms where accessing the state of log files as global variables. If we had kept LogFiles as a global variable, SPARK rules would have required that its invariant is checked on entry and exit from this subprograms. Instead, we changed the signature of these local subprograms to take LogFiles as an additional parameter, on which the invariant needs not hold.
Other transformations on contracts
A few other transformations were needed to make contracts provable with SPARK 2014. In particular, it was necessary to change a number of "and" logical operations into their short-circuit version "and then". See for example this part of the precondition of Processing in tismain.adb:
(if (Admin.IsDoingOp(TheAdmin) and
Admin.TheCurrentOp(TheAdmin) = Admin.OverrideLock)
then
Admin.RolePresent(TheAdmin) = PrivTypes.Guard)
The issue was that calling TheCurrentOp requires that IsDoingOp holds:
function TheCurrentOp (TheAdmin : T) return OpT
with Global => null,
Pre => IsDoingOp (TheAdmin);
Since "and" logical operation evaluates both its operands, TheCurrentOp will also be called in contexts where IsDoingOp does not hold, thus leading to a precondition failure. The fix is simply to use the short-circuit equivalent:
(if (Admin.IsDoingOp(TheAdmin) and then
Admin.TheCurrentOp(TheAdmin) = Admin.OverrideLock)
then
Admin.RolePresent(TheAdmin) = PrivTypes.Guard)
We also added a few loop invariants that were missing.
What about security?
You can read the original Tokeneer report for a description of the security properties that were provably enforced through formal verification.
To demonstrate that indeed formal verification brings assurance that some security vulnerabilities are not present, we have seeded four vulnerabilities in the code, and reanalyzed it. The analysis of GNATprove (either through flow analysis or proof) detected all four: an information leak, a back door, a buffer overflow and an implementation flaw. You can see that in action in this short 4-minutes video.