Address verification

Verifying Deposit Addresses

The addresses REST API endpoints return signatures of the address strings and other metadata that prove the address was generated by Anchorage Digital for your organization.

It is critical that clients verify the address signature and any accompanying metadata before the address is used, to confirm the address authenticity and integrity.

Address Signature Schemes

The API supports two address verification schemes:

  • V1 Address Signatures - The original scheme involves verifying a signature against a public key that is unique to each organization. This public key remains fixed for the lifetime of the organization and is distributed on request by Anchorage Digital out-of-band. While the public key does not need to be kept confidential, it must be kept tamper-proof.
  • V2 Address Signatures - The newer scheme involves verifying a signature against the public key of the leaf certificate of a X509 certificate chain returned alongside the signature, and verifying the certificate chain itself against the Anchorage Digital Address Signing Root CA, provided below, which must be hard-coded by API clients.

You can determine which scheme an address uses by checking the signatureVersion field.

V1 Address Signature Verification

The steps for verifying V1 address signatures are as follows:

  1. Check the validity of the signature:

    a. Decode the addressSignaturePayload field from hex to bytes.

    b. Decode the signature field from hex to bytes.

    c. Using the fixed public key for this organization, verify that signatureBytes is a valid Ed25519 signature of the addressSignaturePayloadBytes.

  2. Verify the signed address matches the address to be used:

    a. Decode the addressSignaturePayload field from hex to bytes.

    b. Parse the bytes as a JSON object.

    c. Verify the address to be used matches the value of the TextAddress property from the JSON object.

Note: It is not sufficient to validate the signature without also validating that the address contained in the JSON decoded from the addressSignaturePayload matches the address to be used.

V1 Signed Payload Fields

  • TextAddress — The text format of the on-chain address.
{
  "TextAddress": "2N19AcihQ1a4MxQW658UFHTioUNnMkiHPkw"
}

Sample V1 validation code:

package main

import (
	"crypto/ed25519"
	"encoding/hex"
	"encoding/json"
	"fmt"
)

// V1SignedPayload represents the JSON structure in the addressSignaturePayload for V1 signatures
type V1SignedPayload struct {
	TextAddress string `json:"TextAddress"`
}

// verifyV1AddressSignature verifies a V1 address signature.
//
// Parameters:
//   - address: The address string from the API response
//   - addressSignaturePayload: Hex-encoded bytes that were signed
//   - signature: Hex-encoded Ed25519 signature
//   - orgPublicKeyHex: Hex-encoded Ed25519 public key for your organization (obtained out-of-band)
//
// Returns an error if verification fails.
func verifyV1AddressSignature(address, addressSignaturePayload, signature, orgPublicKeyHex string) error {
	// Step 1: Check the validity of 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)
	}

	// Decode the organization public key from hex
	publicKeyBytes, err := hex.DecodeString(orgPublicKeyHex)
	if err != nil {
		return fmt.Errorf("failed to decode organization public key: %w", err)
	}

	if len(publicKeyBytes) != ed25519.PublicKeySize {
		return fmt.Errorf("invalid public key size: got %d bytes, expected %d", len(publicKeyBytes), ed25519.PublicKeySize)
	}

	publicKey := ed25519.PublicKey(publicKeyBytes)

	// Verify the Ed25519 signature
	if !ed25519.Verify(publicKey, payloadBytes, signatureBytes) {
		return fmt.Errorf("signature verification failed")
	}

	// Step 2: Verify the signed address matches the address to be used

	// Parse the payload bytes as JSON
	var signedPayload V1SignedPayload
	if err := json.Unmarshal(payloadBytes, &signedPayload); err != nil {
		return fmt.Errorf("failed to parse signed payload: %w", err)
	}

	// Verify the TextAddress matches
	if signedPayload.TextAddress != address {
		return fmt.Errorf("signed TextAddress does not match: signed=%q, expected=%q", signedPayload.TextAddress, address)
	}

	return nil
}

func main() {
	// Sample API response data
	address := "2N19AcihQ1a4MxQW658UFHTioUNnMkiHPkw"
	addressSignaturePayload := "7b225465787441646472657373223a22324e313941636968513161344d78515736353855464854696f554e6e4d6b6948506b77227d"
	signature := "b18f6848dc0fef01a069e7ac26046383bf5cd130203994dc2d72b5a9097351b1e8b67115b63124fbc8c16673566416a635913c670b676089339c62a7824baa03"

	// Organization public key - obtained out-of-band from Anchorage Digital beforehand
	// Unique per Organization, fixed for the lifetime of that Organization
	// Must be kept tamper-proof
	orgPublicKeyHex := "8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c"

	// Verify the signature
	if err := verifyV1AddressSignature(address, addressSignaturePayload, signature, orgPublicKeyHex); err != nil {
		fmt.Printf("✗ V1 Address signature verification failed: %v\n", err)
		return
	}

	fmt.Println("✓ V1 Address signature verified successfully!")
	fmt.Printf("  Address: %s\n", address)
	fmt.Println("\nYou may now safely use this address for deposits.")
}

V2 Address Signature Verification

Addresses returned with signatureVersion set to V2 also include a certChain field containing an x509 certificate chain in PEM format.

The steps for verifying V2 address signatures are as follows:

  1. Verify the certificate chain:

    a. Parse the certChain field as PEM-encoded x509 certificates

    b. Verify the certificate chain from the leaf to the trusted Anchorage Digital Root CA.

    c. Verify all certificates are valid at the current time (both notAfter and notBefore).

    d. Verify the leaf certificate's Subject Alternative Names include address-provider.anchorage.internal.

    e. Verify the leaf certificate's KeyUsage includes both digitalSignature and nonRepudiation (also known as contentCommitment).

    f. Extract the public key from the leaf certificate. Currently, we only support ed25519 keys, however this is subject to change the future.

Note: This list is not comprehensive for verifying x509 Certificates. Verifiers are expected to follow typical x509 verification procedures as implemented by major verification libraries. However, developers should not assume that all of the following checks are performed by default. Libraries may separate these steps into different function calls or require altering verification options.

Note: The leaf certificate is at the 0th index, followed by zero or more intermediate certificates. The root cert is excluded from this response. The number of certificates in the chain is subject to change.

  1. Verify the signature:

    a. Decode the addressSignaturePayload field from hex to bytes

    b. Decode the signature field from hex to bytes

    c. Using the public key from the leaf certificate, verify that signatureBytes is a valid signature of the addressSignaturePayload bytes.

  2. Verify the signed details:

    a. Parse the addressSignaturePayload bytes as a JSON object

    b. Verify SignatureExpiresAt is greater or equal to the current UTC Unix Timestamp

    c. Verify TextAddress matches the address to be used

    d. Verify VaultId matches your expected Vault ID

    e. Verify NetworkId matches the expected network for this address

Note: The signed payload also includes a NetworkName field for human-readable purposes, which does not need to be verified.

Note: API clients must not use "strict" JSON parsers which will disallow extra properties, as future versions may introduce additional fields.

Note: Anchorage Digital will periodically refresh V2 signatures and our Address Signing Root CA before expiration. The deposit address itself will not change. Only the signature, certificate chain and Root CA will be updated.

V2 Signed Payload Fields

  • TextAddress — The text format of the on-chain address.
  • VaultId — Identifies the vault this address belongs to.
  • NetworkId — Identifies the network that this address can receive deposits on.
  • NetworkName — A human readable version of the NetworkId.
  • SignatureExpiresAt — The time after which the signature should not be trusted.
{
  "VaultId": "dae6089e7c0836705f0562af0f1e4e1f",
  "TextAddress": "bcrt1q709skemgf5skpsnysvgme2s3ztehkutl390yl0wp29lnmum5uw7qg0qrwm",
  "NetworkName": "Bitcoin Regnet",
  "NetworkId": "BTC_R",
  "SignatureExpiresAt": 1769450713
}

Anchorage Digital Address Signing Root CAs

Clients are encouraged to hard-code the appropriate Root CA value for the environment they are making API requests against. It is essential that this value be tamper-proof.

  • Production Environment:
-----BEGIN CERTIFICATE-----
MIIBXTCCAQ+gAwIBAgIUQZI+MSvYTXQHra+3OAKnwAMzotUwBQYDK2VwMCAxHjAcBgNVBAMMFWNhLmFuY2hvcmFnZS5pbnRlcm5hbDAeFw0yNjAxMjYwMDAwMDBaFw0yNzAxMjYwMDAwMDBaMCAxHjAcBgNVBAMMFWNhLmFuY2hvcmFnZS5pbnRlcm5hbDAqMAUGAytlcAMhADTh1nctgIHtAKNW8ww/bY606pJ3OP2dyZYcQrU2kG5jo1swWTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwICBDAUBgorBgEEAYaNHwEBBAYWBHJvb3QwIAYDVR0RBBkwF4IVY2EuYW5jaG9yYWdlLmludGVybmFsMAUGAytlcANBANkkdudEjH9RTKbRAxrRXyMSS/TgmdSrAVYOZzoRDJlyc+5oD+a0pmmwWVe86xZi37YbN1GzVlXcJAPpV6ceEQU=
-----END CERTIFICATE-----
  • Staging Environment:
-----BEGIN CERTIFICATE-----
MIIBXDCCAQ6gAwIBAgITOfTQ4rYUsghgvdl8YCJSC67uGDAFBgMrZXAwIDEeMBwGA1UEAwwVY2EuYW5jaG9yYWdlLmludGVybmFsMB4XDTI2MDEyNDAwMDAwMFoXDTI3MDEyNDAwMDAwMFowIDEeMBwGA1UEAwwVY2EuYW5jaG9yYWdlLmludGVybmFsMCowBQYDK2VwAyEAPlBo2/+kPPL0WRpT+B/yHsU25AN/M6HP2bzC61yHb4ajWzBZMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgIEMBQGCisGAQQBho0fAQEEBhYEcm9vdDAgBgNVHREEGTAXghVjYS5hbmNob3JhZ2UuaW50ZXJuYWwwBQYDK2VwA0EAYsJxVI9n42liCF9f+Ou7uuC1QGFwaHwFsfOm0WFofSlE1trWqzj4ruzjPYSRJc8Ht2A7XCAfXkG0mzKpL/wQDg==
-----END CERTIFICATE-----

Sample V2 validation code:

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+P7BbefE1xAHg0hcwBQYDK2VwMAAwHhcNMjYwMTI2MTcwNDEzWhcNMjcwMTI2MTcwNTEzWjAuMSwwKgYDVQQDEyNhZGRyZXNzLXByb3ZpZGVyLmFuY2hvcmFnZS5pbnRlcm5hbDAqMAUGAytlcAMhAPsgM70aWFYsZaLHawtYJpl42BkiTLyCq96+OXe4FxrVo3EwbzAOBgNVHQ8BAf8EBAMCBsAwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBS0usSFeB2gjC+wcowtxN3MeKSH7zAuBgNVHREEJzAlgiNhZGRyZXNzLXByb3ZpZGVyLmFuY2hvcmFnZS5pbnRlcm5hbDAFBgMrZXADQQCXmvIkuPnUgCHxWmFmzvgWdv9lUlt84oZCel+OeJW9n8PR88tGxAcD1E3+KDBXVpO0GcRA0W9+xqqICAo2ROEJ
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIBKDCB26ADAgECAhRGsD05KldIse+uIEa976AijTqlxjAFBgMrZXAwADAeFw0yNjAxMjYxNzA0MTNaFw0yNzAxMjYxNzA1MTNaMAAwKjAFBgMrZXADIQCNpyY5Sr21FHNvvLkBKG8AEMKdhqtajmV5d2QaZlmtAqNnMGUwDgYDVR0PAQH/BAQDAgIEMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFLS6xIV4HaCML7ByjC3E3cx4pIfvMCAGA1UdEQEB/wQWMBSCEmFuY2hvcmFnZS5pbnRlcm5hbDAFBgMrZXADQQCIgw6kLMIwhd3ACjG03cJ5z/ZZp8aXXycFq2ZC9TLhieJ3rncyMH6ZdyJ3Ai1eVaHs4vnDCv54Vdh83vvSky4K
-----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/ExuyW2DAFBgMrZXAwADAeFw0yNjAxMjYxNzA0MTNaFw0yNzAxMjYxNzA1MTNaMAAwKjAFBgMrZXADIQB+gEnytXKnuAMonIWGWnB0qyTqa0aw3l9u5VRbu86UgaNaMFgwDgYDVR0PAQH/BAQDAgIEMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOj64tL1teJHkojsiblnnK34Tw+EMBYGA1UdEQEB/wQMMAqCCGludGVybmFsMAUGAytlcANBAAg2IcVEXmWKSivhUNSatNfMmASxi83QscIuyP/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.")
}

Did this page help you?