vfsecure: verifying model authenticity in the AM supply-chain
The additive manufacturing supply-chain has increasingly decentralized, giving rise to several vulnerabilities, allowing bad actors to inject themselves and tamper with designs. Previous work was done on embedding an authentication code in 3D models. This was taken a step further to introduce geometric obfuscation and encrypted data, allowing a part with an embedded code to act as a self-authenticating component with dual-layer security. The obfuscation prevents a third party from easily detecting and reading a code from the component. In the event in which this is bypassed, the encrypted data, if discovered, is meant to be unusable by an external party. Only the manufacturer is capable of verifying the authenticity of the code, requiring no additional information outside what is provided in the file itself. The verification process involves corroboration of its present state against its untampered state. The system is designed to detect tampering during the computer-aided design (CAD) stage as well as after tessellation (STL); further, the embedded code maintains the ability to act as a unique signature to an authentic product even once the part is additively manufactured.
We developed our project following simple object-oriented principles. As a result, we can describe our process by simply walking through the
main functions in our scripts.
generator.py script does
In very simple terms, it takes an STL object, shuffles the faces and vertices, generates a hash value from this shuffled data, and finally hides this hash value inside a 3D data matrix, which is embedded back into the shuffled STL object, resulting in something like this:
Looking at the main function
We first create an object of the
generator class. It holds some variables related to the data matrix that will be stored in the final object, along with some attributes of the spheres that make up the 3D data matrix.
mesh = generator()
shufflePart() method takes the original object and shuffles the faces and vertices to generate a re-ordered STL file. The
genHash() method reads the shuffled STL and generates a SHA-256 hash.
mesh.shufflePart(filename=args.filename) # generate hash from shuffled part mesh.genHash()
genKey() method generates a random 4 digit number which is then encoded into a 10x10 data matrix using
The newly generated datamatrix is then read and slightly altered in order to obtain a bitmap of the matrix. In the next step,
CODExy() generates XY coordinates for the datamatrix given a new size constraint (enlarge) and shape constraint (sphere).
FOGxy() then places additional spheres, within the constrained region, to obfuscate the datamatrix so that it is not easily identifiable.
mesh.readMatrix() generator.CODExy() mesh.FOGxy()
The matrix points and the obfuscation points are then each assigned a Z value keeping within the constraints of the sphere.
Now for the
genRotVal(): We look at the 4 digit key we originally generated. The first two digits are the iD of the “model” sphere and the second two digits are the offset value to be added each time, then modded by the number of spheres, to get the iD of the remaining spheres.
|Key||Total Spheres||Model iD||Remaining Spheres iD|
|0811||30||8||11, 22, 3, 14, 25…|
We then randomly rotate the “model” sphere. Next, each of the remaining spheres is rotated by a certain X and Z angle obtained from two hexadecimals in the hash, in reference to the “model” sphere. As a result, each rotated sphere holds two hexadecimal values from the hash in its angle of rotation.
readModel() method is essentially just loading the preset, base sphere which will be cloned and placed in the XYZ coordinates, and potentially rotated, in the 3D data matrix.
mesh.assignZ() mesh.genRotVal() mesh.readModel()
Once the rotation values are known and the preset sphere is obtained, the remaining code simply copies each sphere, places it into its correct XYZ location and rotates it to store two hash hexadecimals if its iD fits the key. We then save this “fog” of spheres containing a 3D data matrix and embed it into the larger object. The viewing angle to scan the data matrix is a value picked by the generator and needs to be passed as a separate key, called the offset, to the decoder.
# initialize array for normals generator.normals = np.zeros([mesh.numFaces*mesh.numSpheres,3]) # initialize array for shiftedVertices generator.shiftedVertices = np.zeros([mesh.numVertices*mesh.numSpheres,3]) # shift each sphere by its origin value # The boolean controls rotation: True = rotate; False = no rotate; for i in range(mesh.numSpheres): #numSpheres mesh.shift(i, True) # write out STL file mesh.writeSTL()
decoder.py script does
The decoder script extracts then reads the embedded 3D matrix to obtain the key. Given the key, we know which spheres to check the rotation values of. From there, we obtain the rotated angles to piece together the stored hash data. We then compute the hash data of the STL object (from which the 3D data matrix was extracted) and compare it to the calculated hash from the rotations. If the parts match, then the part is authentic.
Again, we first create an object of the decoder class, containing several methods to manipulate the STL and carry some information about the geometry.
readSTL() then extracts the number of faces, vertices, and normal vectors from the object.
object = decoder() object.readSTL(args.filename)
Next, we search through the entire structure to find the different pieces in the object. This is essentially the larger geometry along with the smaller spheres embedded within which make up the obfuscated 3D data matrix. In doing so, we make the assumption that the geometry with the largest volume is the main object, while all the remaining pieces are part of the 3D data matrix. Hence when we run the
extractForm() methods, we are splitting these two pieces from each other.
for rowloc in range(int(object.numTriangles)): formExist = object.checkForm(rowloc) if (formExist == False): form = object.findForm(rowloc, formTag) formTag += 1 object.extractCode(object.volumeTag) object.extractForm(object.volumeTag)
Once we have the 3D data matrix extracted from the object, we can then scan the data matrix using the offset provided. Having scanned the data matrix, we can obtain the key the matrix contains. With this new key, we can use the normals of the spheres and compare them to the model sphere (told to us by the first two digits of the key), to build the hash function.
We can calculate the hash of the larger object from which we had extracted the 3D data matrix. This is compared to the hash we have learned from the rotations of the spheres. If the two hashes match, then the part was not altered at any point.