Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added eip712 signature version to signed NFT attestation #210

Merged
merged 4 commits into from
Mar 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions data-modules/output/asn/SignedNFTAttestation.asn
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ IMPORTS
AlgorithmIdentifier
FROM AuthenticationFramework;

-- Version 1 or 2, newer uses EIP712 --
SignedNFTAttestation ::= SEQUENCE {
nftAttestation NFTAttestation,
signingVersion INTEGER OPTIONAL,
Expand Down
1 change: 1 addition & 0 deletions data-modules/src/SignedNFTAttestation.asd
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<import name="AlgorithmIdentifier"
schemaLocation="AuthenticationFramework.asd"/>

<!-- VERSION 1 or 2 since newer versions do not use ASN but instead EIP for the signing -->
<namedType name="SignedNFTAttestation">
<type>
<sequence>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public class NFTSmartContractTest {
SignedIdentifierAttestation signed = new SignedIdentifierAttestation(att, issuerKeys);
*/
static SignedIdentifierAttestation attestation;
private SignedNFTAttestation nftAttestation;
private LegacySignedNFTAttestation nftAttestation;
private final SmartContract contract = new SmartContract();
// the URL as King Mida's public ID, plus a label (in case of twitter, the permanent numeric ID)
static final String labeledURI = "https://twitter.com/zhangweiwu 205521676";
Expand Down Expand Up @@ -58,7 +58,7 @@ public void checkNFTSmartContract() throws Exception

NFTAttestation nftAtt = new NFTAttestation(attestation, myNFTs);
//construct SignedNFTAttestation using subject key
nftAttestation = new SignedNFTAttestation(nftAtt, subjectKeys); // <-- signing step, NFT attestation is signed by owner of identifier, referenced below
nftAttestation = new LegacySignedNFTAttestation(nftAtt, subjectKeys.getPrivate()); // <-- signing step, NFT attestation is signed by owner of identifier, referenced below

System.out.println("DER: " + Numeric.toHexString(nftAttestation.getDerEncoding()));

Expand All @@ -69,7 +69,7 @@ public void checkNFTSmartContract() throws Exception
assertTrue(nftAttestation2.verify());

//Generate SignedNFTAttestation using the reconstructed NFTAttestation and the extracted Ethereum signature
SignedNFTAttestation signedNFTAttestation2 = new SignedNFTAttestation(nftAttestation2, subjectKeys);
LegacySignedNFTAttestation signedNFTAttestation2 = new LegacySignedNFTAttestation(nftAttestation2, subjectKeys.getPrivate());
assertTrue(signedNFTAttestation2.checkValidity());
assertTrue(nftAttestation.checkValidity());
assertArrayEquals(signedNFTAttestation2.getUnsignedAttestation().getDerEncoding(), nftAtt.getDerEncoding());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package io.alchemynft.attestation;

import java.io.IOException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.tokenscript.attestation.core.ExceptionUtil;
import org.tokenscript.attestation.eip712.Eip712ObjectSigner;
import org.tokenscript.attestation.eip712.Eip712ObjectValidator;
import org.tokenscript.eip712.FullEip712InternalData;

public class Eip712SignedNFTAttestation implements InternalSignedNFTAttestation {
private static final Logger logger = LogManager.getLogger(Eip712SignedNFTAttestation.class);
public static final String DEFAULT_DOMAIN = "https://autographnft.io";

private Eip712ObjectValidator<NFTAttestation> validator;

private final NFTAttestation att;
private final String signature;
private final String signedEIP712;

public Eip712SignedNFTAttestation(NFTAttestation att, AsymmetricKeyParameter subjectSigningKey) {
try {
NFTAttestationDecoder decoder = new NFTAttestationDecoder(
att.getSignedIdentifierAttestation().getAttestationVerificationKey());
validator = new Eip712ObjectValidator<NFTAttestation>(decoder, new NFTAttestationEncoder(),
DEFAULT_DOMAIN);
Eip712ObjectSigner eipSigner = new Eip712ObjectSigner(subjectSigningKey,
new NFTAttestationEncoder());
this.signedEIP712 = eipSigner.buildSignedToken(att, DEFAULT_DOMAIN);
this.att = validator.retrieveUnderlyingObject(signedEIP712);
this.signature = eipSigner.getSignatureFromJson(signedEIP712);
} catch (IOException e) {
throw ExceptionUtil.throwException(logger, new IllegalArgumentException("Could not decode underlying NFTAttestation"));
}
constructorCheck();
}

public Eip712SignedNFTAttestation(String signedEIP712, AsymmetricKeyParameter identifierAttestationVerificationKey) throws IOException {
NFTAttestationDecoder decoder = new NFTAttestationDecoder(identifierAttestationVerificationKey);
validator = new Eip712ObjectValidator<NFTAttestation>(decoder, new NFTAttestationEncoder(), DEFAULT_DOMAIN);
this.signedEIP712 = signedEIP712;
this.att = validator.retrieveUnderlyingObject(signedEIP712);
this.signature = validator.getSignatureFromJson(signedEIP712);
constructorCheck();
}

private void constructorCheck() {
if (!verify()) {
throw ExceptionUtil.throwException(logger, new IllegalArgumentException("The NFTAttestation is invalid"));
}
}

/**
* Returns the public key of the NFTattestation signer
*/
public AsymmetricKeyParameter getNFTAttestationVerificationKey() {
return validator.retrieveUserPublicKey(signedEIP712, FullEip712InternalData.class);
}

public String getSignedEIP712() {
return signedEIP712;
}

@Override
public NFTAttestation getUnsignedAttestation() {
return att;
}

public String getSignature() {
return signature;
}

/**
* Checks the validity of the underlying NFTAttestation
*/
@Override
public boolean checkValidity() {
return att.checkValidity();
}

/**
* Verifies the entire EIP712 request, including domain, timestamp, usage string, signature and the
* @return
*/
@Override
public boolean verify() {
return validator.validateRequest(signedEIP712);
}

/**
* Returns the ASN encoding of the underlying NFTAttestation
*/
@Override
public byte[] getDerEncoding() {
return att.getDerEncoding();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.alchemynft.attestation;

import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.tokenscript.attestation.CheckableObject;

public interface InternalSignedNFTAttestation extends CheckableObject {
AsymmetricKeyParameter getNFTAttestationVerificationKey();
NFTAttestation getUnsignedAttestation();
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package io.alchemynft.attestation;

import java.io.IOException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DERBitString;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.tokenscript.attestation.core.CompressedMsgSignature;
import org.tokenscript.attestation.core.ExceptionUtil;
import org.tokenscript.attestation.core.PersonalSignature;
import org.tokenscript.attestation.core.Signature;

public class LegacySignedNFTAttestation implements InternalSignedNFTAttestation {
private static final Logger logger = LogManager.getLogger(LegacySignedNFTAttestation.class);
public static final int DEFAULT_SIGNING_VERSION = 2;

private final NFTAttestation nftAtt;
private final Signature signature;
private final int signingVersion;

public LegacySignedNFTAttestation(NFTAttestation nftAtt, AsymmetricKeyParameter subjectSigningKey) {
this(nftAtt, subjectSigningKey, DEFAULT_SIGNING_VERSION);
}

public LegacySignedNFTAttestation(NFTAttestation nftAtt, AsymmetricKeyParameter subjectSigningKey, int signingVersion) {
this.nftAtt = nftAtt;
this.signature = makeSignature(subjectSigningKey, signingVersion);
this.signingVersion = signingVersion;

if (!verify()) {
throw ExceptionUtil.throwException(logger, new IllegalArgumentException("The signature is not valid"));
}
}

/**
* Constructor used for when we supply the signature separately
*/
public LegacySignedNFTAttestation(NFTAttestation NftAtt, Signature signature) {
this.nftAtt = NftAtt;
this.signature = signature;
this.signingVersion = determineSigningVersion();
if (!verify()) {
throw ExceptionUtil.throwException(logger, new IllegalArgumentException("The signature is not valid"));
}
}

public LegacySignedNFTAttestation(byte[] derEncoding, AsymmetricKeyParameter identifierAttestationVerificationKey) throws IOException {
ASN1InputStream input = new ASN1InputStream(derEncoding);
ASN1Sequence asn1 = ASN1Sequence.getInstance(input.readObject());
input.close();
int currentPos = 0;
ASN1Sequence nftEncoding = ASN1Sequence.getInstance(asn1.getObjectAt(currentPos++));
this.nftAtt = new NFTAttestation(nftEncoding.getEncoded(), identifierAttestationVerificationKey);
if (asn1.getObjectAt(currentPos) instanceof ASN1Integer) {
this.signingVersion = ASN1Integer.getInstance(asn1.getObjectAt(currentPos++)).intValueExact();
} else {
// If signingVersion is not present we default to version 1
this.signingVersion = 1;
}
// todo this actually not used
AlgorithmIdentifier algorithmIdentifier = AlgorithmIdentifier.getInstance(asn1.getObjectAt(currentPos++));
DERBitString signatureEnc = DERBitString.getInstance(asn1.getObjectAt(currentPos++));
this.signature = makeSignature(signatureEnc.getBytes(), signingVersion);
}

private int determineSigningVersion() {
if (signature instanceof PersonalSignature) {
return 1;
}
else if (signature instanceof CompressedMsgSignature) {
return 2;
} else {
throw ExceptionUtil.throwException(logger, new IllegalArgumentException("Unexpected signature type used"));
}
}

Signature makeSignature(byte[] encodedBytes, int signingVersion) {
if (signingVersion == 1) {
return new PersonalSignature(encodedBytes);
}
else if (signingVersion == 2) {
return new CompressedMsgSignature(encodedBytes, SignedNFTAttestation.PREFIX_MSG, SignedNFTAttestation.POSTFIX_MSG);
} else {
throw ExceptionUtil.throwException(logger, new IllegalArgumentException("Unknown signing version"));
}
}

Signature makeSignature(AsymmetricKeyParameter key, int signingVersion) {
if (signingVersion == 1) {
return new PersonalSignature(key, nftAtt.getDerEncoding());
}
else if (signingVersion == 2) {
return new CompressedMsgSignature(key, nftAtt.getDerEncoding(), SignedNFTAttestation.PREFIX_MSG, SignedNFTAttestation.POSTFIX_MSG);
} else {
throw ExceptionUtil.throwException(logger, new IllegalArgumentException("Unknown signing version"));
}
}

@Override
public NFTAttestation getUnsignedAttestation() {
return nftAtt;
}

public Signature getSignature() {
return signature;
}

/**
* Returns the public key of the NFTattestation signer
*/
@Override
public AsymmetricKeyParameter getNFTAttestationVerificationKey() {
return nftAtt.getAttestedUserKey();
}

@Override
public byte[] getDerEncoding() {
return constructSignedAttestation(this.nftAtt, this.signature.getRawSignature());
}

byte[] constructSignedAttestation(NFTAttestation unsignedAtt, byte[] signature) {
try {
byte[] rawAtt = unsignedAtt.getDerEncoding();
ASN1EncodableVector res = new ASN1EncodableVector();
res.add(ASN1Primitive.fromByteArray(rawAtt));
// Only include version number if it is greater than 1
if (signingVersion > 1) {
res.add(new ASN1Integer(signingVersion));
}
res.add(unsignedAtt.getSigningAlgorithm());
res.add(new DERBitString(signature));
return new DERSequence(res).getEncoded();
} catch (Exception e) {
throw new RuntimeException(e);
}
}

@Override
public boolean checkValidity() {
return getUnsignedAttestation().checkValidity();
}

@Override
public boolean verify() {
if (!signature.verify(nftAtt.getDerEncoding(), getNFTAttestationVerificationKey())) {
return false;
}
if (!nftAtt.verify()) {
return false;
}
return true;
}
}
18 changes: 15 additions & 3 deletions src/main/java/io/alchemynft/attestation/NFTAttestation.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.crypto.util.PublicKeyFactory;
import org.tokenscript.attestation.AttestedKeyObject;
import org.tokenscript.attestation.ERC721Token;
import org.tokenscript.attestation.SignedIdentifierAttestation;
import org.tokenscript.attestation.core.ASNEncodable;
import org.tokenscript.attestation.core.Validateable;
import org.tokenscript.attestation.core.ExceptionUtil;

public class NFTAttestation implements ASNEncodable, Validateable {
public class NFTAttestation extends AttestedKeyObject {
private static final Logger logger = LogManager.getLogger(NFTAttestation.class);

private final SignedIdentifierAttestation signedIdentifierAttestation;
Expand Down Expand Up @@ -80,4 +81,15 @@ public boolean checkValidity() {
public boolean verify() {
return signedIdentifierAttestation.verify();
}

@Override
public AsymmetricKeyParameter getAttestedUserKey() {
try {
return PublicKeyFactory.createKey(
getSignedIdentifierAttestation().getUnsignedAttestation()
.getSubjectPublicKeyInfo());
} catch (IOException e) {
throw ExceptionUtil.makeRuntimeException(logger, "Could not restore key from signed signed attestation", e);
}
}
}
34 changes: 34 additions & 0 deletions src/main/java/io/alchemynft/attestation/NFTAttestationDecoder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.alchemynft.attestation;

import java.io.IOException;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.tokenscript.attestation.ERC721Token;
import org.tokenscript.attestation.ObjectDecoder;
import org.tokenscript.attestation.SignedIdentifierAttestation;

public class NFTAttestationDecoder implements ObjectDecoder<NFTAttestation> {
private final AsymmetricKeyParameter identifierAttestationVerificationKey;
public NFTAttestationDecoder(AsymmetricKeyParameter identifierAttestationVerificationKey) {
this.identifierAttestationVerificationKey = identifierAttestationVerificationKey;
}

@Override
public NFTAttestation decode(byte[] encoding) throws IOException {
ASN1InputStream input = new ASN1InputStream(encoding);
ASN1Sequence asn1 = ASN1Sequence.getInstance(input.readObject());
input.close();
ASN1Sequence attestationEnc = ASN1Sequence.getInstance(asn1.getObjectAt(0)); //root attestation, should be signed att
SignedIdentifierAttestation signedIdentifierAttestation = new SignedIdentifierAttestation(attestationEnc.getEncoded(), identifierAttestationVerificationKey);

ASN1Sequence tokensEnc = ASN1Sequence.getInstance(asn1.getObjectAt(1));
DERSequence tokens = DERSequence.convert(tokensEnc);
ERC721Token[] erc721Tokens = new ERC721Token[tokens.size()];
for (int i = 0; i< erc721Tokens.length; i++) {
erc721Tokens[i] = new ERC721Token(tokens.getObjectAt(i).toASN1Primitive().getEncoded());
}
return new NFTAttestation(signedIdentifierAttestation, erc721Tokens);
}
}
Loading