AWS Database Blog

Build NFT metadata access control with Ethereum signatures and AWS Lambda authorizers

Non-fungible tokens (NFTs) have captured global attention as a mechanism for creating one-of-a-kind digital assets that can be instantly verified as authentic, easily exchanged between users, and made infinitely programmable such that NFTs can be used for a variety of use cases and industries.

At its core, NFTs are a form of digital asset or token accounted for on a blockchain network via a smart contract. This smart contract is comprised of code that allows users to mint (create), burn (delete), and exchange NFTs on public blockchains like Ethereum, where an NFT is in many cases as simple as a key-value pair stored on that blockchain. This key-value pair is often a unique identifier (the token) mapped to an owner’s blockchain wallet address.

Naturally, popular implementations of NFTs such as digital art or video game items require other metadata and content to function properly, which is stored on an off-chain data store varying from centralized options like Amazon Simple Storage Service (Amazon S3) to decentralized file storage systems like Interplanetary File System (IPFS). In addition to the key-value pair for token identifier and owner address, NFT smart contracts often store a URI that points to that particular token’s metadata or content. As a result, this public URI can be used by virtually anyone to access the content associated with an NFT, which is perfectly reasonable for some use cases but unviable for others, such as where an NFT entitles its owner to an exclusive piece of content that they don’t wish to share with the public (such as a movie, song, or piece of art).

In this post, we outline a solution to this challenge, using AWS Lambda authorizers and Ethereum signatures to authorize access to NFT metadata stored in Amazon S3, allowing only the owner of that NFT on the Ethereum blockchain to see the content stored in Amazon S3.

How to control access to NFT metadata with AWS services

NFTs are comprised of three major components:

  • On-chain data marshaled by smart contracts according to established standards (for example, ERC721 or ERC1155)
  • A JSON metadata file retrievable by the token URI for a given NFT that defines the attributes and content the NFT represents (such as the stats for an in-game character)
  • Optionally, a media file with additional content referenced in the JSON metadata file

As described earlier, an NFT is minted on a public blockchain, and anyone can publicly visit the stored NFT metadata URI regardless of the content within. Today, most NFTs point to public URIs where the metadata and content is publicly accessible. For many use cases, content must be accessible to the owner of the NFT at any given time, but be restricted to the general public if required by the owner.

In this solution, you can store the NFT metadata on Amazon S3 and expose a URI to access that content via Amazon API Gateway backed by a Lambda authorizer that determines who can access that content. With this authorizer, we can verify and authorize requests to retrieve the metadata file by verifying a signature from an Ethereum-compatible blockchain wallet to ensure only the owner (or other authorized party) is able to access the metadata file.

Why use Amazon S3 to store NFT metadata?

Amazon S3 is an object storage service offering industry-leading scalability, data availability, security, and performance. Amazon S3 and its configurable storage tiering is an economical choice for storing data at high scale and for a long period of time.

For storing NFT metadata, Amazon S3 enables an NFT creator to store both metadata JSON files as well as supporting content in an easily retrievable way, either directly from Amazon S3 from the web, or integrating a global content delivery network such as Amazon CloudFront.

Other decentralized alternatives for storing NFT metadata and content like IPFS usually require the owner to maintain their own IPFS node or shoulder the burden of paying for a pinning service that will ensure files remain available on IPFS. In IPFS, files that aren’t retrieved often can be dropped out of IPFS node’s storage, which requires additional effort to pin those files on IPFS nodes to ensure availability of that content.

This added complexity and cost can make it difficult for someone new to decentralized storage options to get started. For example, compared to a popular IPFS pinning service that costs $0.15 per GB, Amazon S3 costs merely $0.02 per GB to store the same types of content in the us-east-1 Region, representing almost 85% savings. Naturally, cost is not the only factor—a cornerstone of the NFT community is decentralization, and there are instances where either decentralized options like IPFS or centralized options like Amazon S3 are ideal choices.

What constitutes an Ethereum-compatible blockchain wallet?

The Ethereum blockchain is an account-based blockchain, meaning the underlying ledger stores states related to user balances and application data mapped by an address that uniquely identifies each user. That address is derived from a public key in a public/private key pair. Following the principles of public key cryptography, the public key can be used to verify that a given transaction or message has been signed by its corresponding private key—this process is the crux of signature verification methods outlined in this post. Specifically, a user can prove the ownership of an address (derived from their public key) simply by signing a message with their private key.

An Ethereum-compatible blockchain wallet encapsulates this public/private key pair and uses the Elliptic Curve Digital Signature Algorithm (ECDSA) secp256k1 to produce digital signatures for use in the protocol, which is the same cryptography implementation used in Bitcoin. A blockchain wallet contains a user’s private key and public key derived addresses, and allows a user to sign transactions and view ownership of cryptocurrency or other digital assets owned by their addresses.

In this post, our wallet’s private key is what we use to prove ownership of a given address, which in turn proves ownership of a given NFT. We achieve this by producing a signed message using the Ethereum-compatible blockchain wallet private key.

Using Ethereum-compatible blockchain wallets to sign and verify messages

With an Ethereum-compatible blockchain wallet and its corresponding private key, you can produce a signed message to prove ownership of a given address, which in turn can be used to verify your ownership in a given digital asset on the blockchain. Again, recall that a user’s address is derived from the public key in the wallet’s public/private key pair, and that address is used to record ownership of digital assets on the blockchain ledger.

Signed messages conform to a general standard format comprised of the message itself, the signature, and the address of the signer. The following code is an example of a message signed using an Ethereum-compatible blockchain wallet:

{
 "message":"129382319",
 "signature":"0x23f37ea5226f18d69670a13490c2acaaf22c6a91b4137c198494377593159a35026f655d0e361849d03518feeadf2dec7df49fe7f33dfc13d4f797095900782d1b",
 "address":"0xcf2C679AC6cb7de09Bf6BB6042ecCF05b7FA1394"
}

To verify this message, the signature field and the original message, constituted as hashes, are provided as inputs to an Ethereum signature verification function such as Ethers.js utils.recoverAddress to validate that the message was in fact signed by the owner of the address in question. If the verification function returns the address provided in the original message, it can be considered a valid signature to verify the ownership of a given address, and in turn, a given NFT. Remember, signed messages in Ethereum are primarily used to unequivocally prove ownership of a given address by proving who owns the private key used to derive it. Nevertheless, the recipient might still be at risk of a replay attack if it’s the only method used to verify the sender’s identity (see security considerations).

Solution overview

For your reference, the full source code for this sample solution can be found in the GitHub repo.

The following diagram illustrates our solution architecture.

Prerequisites

If you haven’t yet installed Node.js or NPM, do so before attempting to run the React app. You can install Node.js or NPM using Node Version Manager.

To deploy the core components of the NFT access control architecture, you use the AWS Serverless Application Model (AWS SAM) command line interface tools. To install the AWS SAM CLI, refer to Installing the AWS SAM CLI. From the serverless/ directory of the repository, run the following AWS SAM CLI command to deploy the infrastructure:

cd serverless/ 
sam build
sam deploy --guided --capabilities CAPABILITY_NAMED_IAM

To produce Ethereum signatures, we use a front-end application. Deploy and start front end app by running following command from client/ directory

npm i
npm start

Access the front-end app by navigating to localhost:3000 from a modern web browser (such as Google Chrome) with MetaMask browser extension. You will be prompted to sign a message with your Ethereum-compatible blockchain wallet via MetaMask.

Create an Ethereum-compatible blockchain wallet for deploying your NFT smart contract

To sign and pay for a transaction on the Ethereum Rinkeby network, you need an Ethereum wallet. An Ethereum-compatible blockchain wallet is comprised of a private/public key pair. You can create your own Ethereum-compatible blockchain wallet programmatically using popular Ethereum libraries Web3 and Ethers.

Note that this method for creating and managing an Ethereum-compatible blockchain wallet private key is not suitable for production spending keys. Do not use this wallet for mainnet Ether!

Generate the private key using one of the aforementioned libraries and upload to AWS Systems Manager Parameter Store as an encrypted string under the name ethSystemKey. Make sure the secure string value excludes the first two characters, 0x, of the private key. In this case, we use the wallet created here as a backend signer to deploy smart contracts and mint NFTs on behalf of users.

Add some test network Ether for the Ethereum Rinkeby test network using the Rinkeby faucet https://faucet.rinkeby.io/ by entering the Ethereum address generated during wallet creation and requesting test tokens. Special care must be taken with spending keys (private keys), and AWS Systems Manager might not be adequate for wallets that can be used to spend funds on mainnet.

If you’re having trouble requesting testnet Ether from the Rinkeby faucet, you may use the Chainlink Faucet to get small amounts of testnet Ether.

Deploy the NFT smart contract (ERC721)

After an Ethereum-compatible blockchain wallet has been created for the backend NFT minting service and funded with testnet Ether, we can deploy the ERC721 token standard smart contract that we use to mint and manage NFTs to the Ethereum testnet. The ERC721 smart contract standard defines methods and functionalities to create and manage NFTs on the blockchain. To deploy the smart contract, first modify the JSON event deploy.json to define the NFT contract details such as the name, ticker symbol, and base URI for the underlying NFT content. The base URI refers to the endpoint where the metadata files reside for each NFT minted via this smart contract. See the following code:

{
    "requestType": "deploy",
    "tokenName": "awsnft",
    "tokenTicker": "MZAN",
    "baseURI": "https://<api>.execute-api.<region>.amazonaws.com /nftapi/assets/"
}
curl -X POST https://<api>.execute-api.<region>.amazonaws.com/nftapi/deploy -H "Content-Type: application/json" -d @deploy.json

After the smart contract is deployed, you receive a response containing the transaction hash (ID) and contract address that you use in the next step to mint an NFT using that smart contract.

Mint an NFT

With the ERC721 smart contract deployed, we can invoke the _safeMint function on the smart contract to mint a new NFT with an incremented unsigned integer token ID. The first NFT created in this smart contract has a token ID of 0, the second has a token ID of 1, and so on.

To mint, modify the mintAddress variable in the mint.json event file. The mintAddress is the Ethereum address (not necessarily the same address as the one that deployed the contract) that the token ownership is transferred to upon the creation of the NFT. In this case, copy the address from your MetaMask wallet and enter it as the address to mint to. See the following code:

{  
    "requestType": "mint",  
    "contractAddress": "<your deployed contract address>",  
    "mintAddress": "<your Ethereum address>",  
    "gasLimit": 9000000,  
    "gasPrice": 99999999999,  
    "metadata": {    
        "description": "useful description",     
        "image": "<your nft image url>",     
        "name": "The best nft"  
        }
}

curl -X POST https://<api>.execute-api.<region>.amazonaws.com/nftapi/mint -H "Content-Type: application/json" -d @mint.json

This minting function mints a new NFT and assigns its owner to the Ethereum address copied from your MetaMask wallet, and stores the metadata attributes defined in mint.json within an S3 bucket for retrieval later. With the Lambda authorizer deployed earlier, this metadata is only accessible by the owner of the NFT. Any requests to retrieve this metadata are authenticated by verifying an Ethereum signature from your Ethereum-compatible blockchain wallet in MetaMask. The private key signature produced by your wallet proves your ownership of the address that owns the NFT corresponding to the requested metadata.

Get the metadata URI

To retrieve the metadata for your new NFT, you must first retrieve the metadata URI that was set in earlier steps. This metadata URI points to an API endpoint through which requests for token metadata stored in Amazon S3 are handled. To get the tokenId from the prior step, you can either find it on etherscan or change the mint configuration to wait for minting full confirmation response (not recommended for production). Once you have the minted tokenId, use it with the following code to request the metadata URI:

curl “https://<api>.execute-api.<region>.amazonaws.com/nftapi/details?contract=<your deployed contract address>&tokenId=<your minted tokenId>

Copy the response, which contains the metadata URI API endpoint concatenated with the token ID provided in the request, and save it in a notepad for later.

Note that in Solidity, the view methods to retrieve NFT balances and URIs can be called with the altered msg.sender property to retrieve URIs, balances, and so on. The msg.sender property sets the origin address for a given smart contract call or transaction.

Sign the message

Next, you use the Ethereum-compatible blockchain wallet via MetaMask that you used to mint the NFT in an earlier step to sign a message that verifies that you’re the owner of the NFT for which you’re requesting metadata for. With the React application running from the earlier setup steps, navigate to localhost:3000 in your web browser. Provide the token ID, metadata ID, and smart contract address of your NFT and choose Sign Message. This triggers a pop-up window from your MetaMask wallet that requires you to accept the signature request. When the request is complete, it generates a JSON signature output that can be used to verify your NFT ownership.

Call the API to retrieve the metadata info

Finally, you can enter the URL for your metadata API (defined in deploy.json) to submit a request to retrieve the token metadata for the NFT. You use the message that was signed with your wallet in the prior step to prove ownership of the NFT and subsequently to authenticate your request for the token metadata via the custom Lambda authorizer that verifies the Ethereum signature you produced with your wallet.

Security considerations

Although the implementation outlined in this post is designed to be easily deployable by the reader, there are additional security considerations to be aware of in your own deployment, including but not limited to:

  • Consider deploying your Lambda functions inside a VPC construct to define clear network boundaries and ease the process of auditing connectivity and access
  • Take special care in managing private keys, including Ethereum wallet private keys, and use a secrets manager or key management mechanism at all times for key storage and signing
  • Consider adding a dynamic challenge to the authorization phase as part of the signing request step to avoid replay attacks
  • During the deployment process, run yarn audit and npm audit commands to be aware of any possible vulnerabilities present in application dependencies

Clean up

To avoid incurring future charges, delete the resources deployed throughout this post:

  1. Empty the S3 bucket that stores the underlying NFT metadata files.
  2. Delete the ethSystemKey parameter from Parameter Store.
  3. Finally, delete the deployed AWS SAM stack using the AWS SAM CLI:
    cd serverless
    sam delete nft-stack

Conclusion

In this post, we presented a way to control access to the underlying metadata and content for an NFT via Ethereum signature verification. The solution utilizes a custom Lambda authorizer and Ethereum signatures to authorize access to NFT metadata stored in Amazon S3, allowing only the owner of that NFT on the Ethereum blockchain to see the content stored in Amazon S3. Protecting the content pertaining to a given NFT provides the owner improved control over the accessibility and distribution of that content and potentially protects the value of the NFT itself.

To learn more about building full-stack NFT applications on AWS, refer to Develop a Full Stack Serverless NFT Application with Amazon Managed Blockchain – Part 1.


About the authors

Forrest Colyer is a blockchain specialist Solutions Architect at AWS. Through his experience with private blockchain solutions led by consortia and public blockchain use cases like NFTs and DeFi, Forrest helps enable customers to identify and implement high-impact blockchain solutions.

Matan Zutta is a Solutions Architect Manager at AWS. He leads and mentors the next generation of architects to assist customers in building and operating their systems in the cloud. Having worked in software development and architecture for more than 15 years in a variety of fields, he has specific passion for emerging technologies and tackling challenging problems.