package main
import (
"crypto/ed25519"
"crypto/x509"
"encoding/hex"
"encoding/json"
"encoding/pem"
"fmt"
"time"
)
// V2SignedPayload represents the JSON structure in the addressSignaturePayload for V2 signatures
type V2SignedPayload struct {
TextAddress string `json:"TextAddress"`
VaultId string `json:"VaultId"`
NetworkId string `json:"NetworkId"`
NetworkName string `json:"NetworkName"`
SignatureExpiresAt int64 `json:"SignatureExpiresAt"` // Unix timestamp
}
// verifyV2AddressSignature verifies a V2 address signature.
//
// Parameters:
// - now: The "current" time. Note that conforming implementations must use
// a trusted source for the current time.
// - address: The address string from the API response
// - addressSignaturePayload: Hex-encoded bytes that were signed
// - signature: Hex-encoded signature
// - certChainPEM: PEM-encoded certificate chain (leaf first, then intermediates)
// - rootCAPEM: PEM-encoded Root CA certificate (hard-coded by client)
// - expectedVaultId: Your Vault ID to verify against the signed VaultId
// - expectedNetworkId: Expected network ID for this address (e.g., "BTC", "ETH")
//
// Returns an error if verification fails.
func verifyV2AddressSignature(
now time.Time,
address, addressSignaturePayload, signature, certChainPEM, rootCAPEM, expectedVaultId, expectedNetworkId string,
) error {
// Step 1: Verify the certificate chain
// Parse the certificate chain from PEM
certs, err := parsePEMCertificates([]byte(certChainPEM))
if err != nil {
return fmt.Errorf("failed to parse certificate chain: %w", err)
}
if len(certs) == 0 {
return fmt.Errorf("certificate chain is empty")
}
leafCert := certs[0]
var intermediateCerts []*x509.Certificate
if len(certs) > 1 {
intermediateCerts = certs[1:]
}
// Parse the Root CA
rootCACerts, err := parsePEMCertificates([]byte(rootCAPEM))
if err != nil {
return fmt.Errorf("failed to parse Root CA: %w", err)
}
if len(rootCACerts) != 1 {
return fmt.Errorf("expected exactly one Root CA certificate, got %d", len(rootCACerts))
}
rootCA := rootCACerts[0]
// Verify the leaf certificate's KeyUsage includes both
// digitalSignature and nonRepudiation (AKA contentCommitment)
if leafCert.KeyUsage&x509.KeyUsageDigitalSignature == 0 {
return fmt.Errorf("leaf certificate KeyUsage missing DigitalSignature")
}
if leafCert.KeyUsage&x509.KeyUsageContentCommitment == 0 {
return fmt.Errorf("leaf certificate KeyUsage missing NonRepudiation (ContentCommitment)")
}
// Verify the certificate chain from leaf to Root CA
roots := x509.NewCertPool()
roots.AddCert(rootCA)
intermediates := x509.NewCertPool()
for _, cert := range intermediateCerts {
intermediates.AddCert(cert)
}
// NOTE: Not all x509 libraries are created equal and are not
// guaranteed to verify exactly the same things!
//
// Always review the library you plan to use and ensure it covers the
// checks described in the User Guide!
//
// For example, the Go implementation checks all Certificates for
// temporal validity (notBefore and notAfter against CurrentTime), for
// valid signatures up the chain, and checks that the Subject
// Alternative Names include the values in DNSNames below.
//
// However it does not check the KeyUsage bits, hence the additional
// checks above.
opts := x509.VerifyOptions{
DNSNames: []string{"address-provider.anchorage.internal"},
Roots: roots,
Intermediates: intermediates,
CurrentTime: now,
// NOTE: This allows for any Extended Key Usage, but does not
// check the Key Usage bits, hence the additional checks above.
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
}
if _, err := leafCert.Verify(opts); err != nil {
return fmt.Errorf("certificate chain verification failed: %w", err)
}
// Extract the public key from the leaf certificate
leafPublicKey, ok := leafCert.PublicKey.(ed25519.PublicKey)
if !ok {
return fmt.Errorf("leaf certificate does not use Ed25519 (got type %T)", leafCert.PublicKey)
}
// Step 2: Verify the signature
// Decode the addressSignaturePayload from hex to bytes
payloadBytes, err := hex.DecodeString(addressSignaturePayload)
if err != nil {
return fmt.Errorf("failed to decode addressSignaturePayload: %w", err)
}
// Decode the signature from hex to bytes
signatureBytes, err := hex.DecodeString(signature)
if err != nil {
return fmt.Errorf("failed to decode signature: %w", err)
}
// Verify the signature using the leaf certificate's public key
if !ed25519.Verify(leafPublicKey, payloadBytes, signatureBytes) {
return fmt.Errorf("signature verification failed")
}
// Step 3: Verify the signed details
// Parse the payload bytes as JSON
var signedPayload V2SignedPayload
if err := json.Unmarshal(payloadBytes, &signedPayload); err != nil {
return fmt.Errorf("failed to parse signed payload: %w", err)
}
// Verify SignatureExpiresAt is not in the past
if now.Unix() > signedPayload.SignatureExpiresAt {
expiryTime := time.Unix(signedPayload.SignatureExpiresAt, 0)
return fmt.Errorf("signature has expired at %s", expiryTime)
}
// Verify TextAddress matches
if signedPayload.TextAddress != address {
return fmt.Errorf("signed TextAddress does not match: signed=%q, expected=%q",
signedPayload.TextAddress, address)
}
// Verify VaultId matches
if signedPayload.VaultId != expectedVaultId {
return fmt.Errorf("signed VaultId does not match: signed=%q, expected=%q",
signedPayload.VaultId, expectedVaultId)
}
// Verify NetworkId matches
if signedPayload.NetworkId != expectedNetworkId {
return fmt.Errorf("signed NetworkId does not match: signed=%q, expected=%q",
signedPayload.NetworkId, expectedNetworkId)
}
return nil
}
// parsePEMCertificates parses PEM-encoded certificates and returns them as a slice
func parsePEMCertificates(pemData []byte) ([]*x509.Certificate, error) {
var certs []*x509.Certificate
for {
block, rest := pem.Decode(pemData)
if block == nil {
break
}
if block.Type != "CERTIFICATE" {
pemData = rest
continue
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate: %w", err)
}
certs = append(certs, cert)
pemData = rest
}
return certs, nil
}
func main() {
// Sample API response data
address := "bcrt1q709skemgf5skpsnysvgme2s3ztehkutl390yl0wp29lnmum5uw7qg0qrwm"
addressSignaturePayload := "7b225661756c744964223a226461653630383965376330383336373035663035363261663066316534653166222c225465787441646472657373223a22626372743171373039736b656d676635736b70736e797376676d653273337a7465686b75746c333930796c30777032396c6e6d756d357577377167307172776d222c224e6574776f726b4e616d65223a22426974636f696e205265676e6574222c224e6574776f726b4964223a224254435f52222c225369676e6174757265457870697265734174223a313736393435303731337d"
signature := "951eb2fb560e660aa9c3d1ccd120d3ad1a19d90d8747347057e48bf174330eb386089e3232d822fd66b8183cce8059c91183afde299b920a0e0c05c5b167360e"
certChainPEM := `-----BEGIN CERTIFICATE-----
MIIBYTCCAROgAwIBAgIUMLKt+K9eFku+P7BbefE1xAHg0hcwBQYDK2VwMAAwHhcN
MjYwMTI2MTcwNDEzWhcNMjcwMTI2MTcwNTEzWjAuMSwwKgYDVQQDEyNhZGRyZXNz
LXByb3ZpZGVyLmFuY2hvcmFnZS5pbnRlcm5hbDAqMAUGAytlcAMhAPsgM70aWFYs
ZaLHawtYJpl42BkiTLyCq96+OXe4FxrVo3EwbzAOBgNVHQ8BAf8EBAMCBsAwDAYD
VR0TAQH/BAIwADAfBgNVHSMEGDAWgBS0usSFeB2gjC+wcowtxN3MeKSH7zAuBgNV
HREEJzAlgiNhZGRyZXNzLXByb3ZpZGVyLmFuY2hvcmFnZS5pbnRlcm5hbDAFBgMr
ZXADQQCXmvIkuPnUgCHxWmFmzvgWdv9lUlt84oZCel+OeJW9n8PR88tGxAcD1E3+
KDBXVpO0GcRA0W9+xqqICAo2ROEJ
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIBKDCB26ADAgECAhRGsD05KldIse+uIEa976AijTqlxjAFBgMrZXAwADAeFw0y
NjAxMjYxNzA0MTNaFw0yNzAxMjYxNzA1MTNaMAAwKjAFBgMrZXADIQCNpyY5Sr21
FHNvvLkBKG8AEMKdhqtajmV5d2QaZlmtAqNnMGUwDgYDVR0PAQH/BAQDAgIEMBIG
A1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFLS6xIV4HaCML7ByjC3E3cx4pIfv
MCAga1UdEQEB/wQWMBSCEmFuY2hvcmFnZS5pbnRlcm5hbDAFBgMrZXADQQCIgw6k
LMIwhd3ACjG03cJ5z/ZZp8aXXycFq2ZC9TLhieJ3rncyMH6ZdyJ3Ai1eVaHs4vn
DCv54Vdh83vvSky4K
-----END CERTIFICATE-----
`
// NOTE: This is a FAKE Root CA used just for this example.
// NOTE: Conforming client implementations should hard-code the real
// Anchorage Digital Address Signing Root CA for the environment they
// are making requests to.
rootCAPEM := `-----BEGIN CERTIFICATE-----
MIIBGzCBzqADAgECAhQ2qQwArneTuF0dbNDs8i/ExuyW2DAFBgMrZXAwADAeFw0y
NjAxMjYxNzA0MTNaFw0yNzAxMjYxNzA1MTNaMAAwKjAFBgMrZXADIQB+gEnytXKn
uAMonIWGWnB0qyTqa0aw3l9u5VRbu86UgaNaMFgwDgYDVR0PAQH/BAQDAgIEMA8G
A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOj64tL1teJHkojsiblnnK34Tw+EMBYGN
A1UdEQEB/wQMMAqCCGludGVybmFsMAUGAytlcANBAAg2IcVEXmWKSivhUNSatNfM
mASxi83QscIuyP/sIW2sRIuCqQJoo9lN6TaxzyV62cQMzthFOCZcgRE+k0JV7Ao=
-----END CERTIFICATE-----
`
// Your Vault ID - obtained from your application context
expectedVaultId := "dae6089e7c0836705f0562af0f1e4e1f"
// Expected network ID for this address
expectedNetworkId := "BTC_R"
// Implementations should use the actual current time
// now := time.Now()
now := time.Unix(1769450600, 0) // Fake time so that this example passes.
// Verify the signature
if err := verifyV2AddressSignature(
now,
address,
addressSignaturePayload,
signature,
certChainPEM,
rootCAPEM,
expectedVaultId,
expectedNetworkId,
); err != nil {
fmt.Printf("✗ V2 Address signature verification failed: %v\n", err)
return
}
fmt.Println("✓ V2 Address signature verified successfully!")
fmt.Printf(" Address: %s\n", address)
fmt.Printf(" Vault ID: %s\n", expectedVaultId)
fmt.Printf(" Network: %s\n", expectedNetworkId)
fmt.Println("\nYou may now safely use this address for deposits.")
}