vfsecure: verifying model authenticity in the AM supply-chain

An approach for verifying model authenticity in the AM supply-chain as part of cybersecurity research at NYU Tandon’s Composite Materials and Mechanics Lab. A project by @niniack & @michaellinares

Check out the code here

preview

Project Abstract

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.


Research Poster

Research Poster for NYU Tandon Undergraduate Summer Research Program 2019
Research Poster for NYU Tandon Undergraduate Summer Research Program 2019

Project Description

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.

What the generator.py script does

preview

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:

preview

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()

The 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()

The genKey() method generates a random 4 digit number which is then encoded into a 10x10 data matrix using genMatrix()

    mesh.genKey()
    mesh.genMatrix()

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.

For example:

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.

The 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()

What the decoder.py script does

preview

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.

Looking at the main function

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 extractCode() and 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.

    object.readDMX()
    object.readRot()

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.

    object.genHash()
    object.compHash()