Skip to main content
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.
Verify the address signature and all accompanying metadata before using any address. This confirms authenticity and integrity.

Signature schemes

The API supports two address verification schemes. Check the signatureVersion field to determine which applies.
SchemeHow it works
V1Verify signature against a fixed Ed25519 public key unique to your organization, distributed out-of-band by Anchorage Digital. Must be kept tamper-proof.
V2Verify signature against the public key of the leaf certificate in an X.509 certificate chain returned with the response, then verify the chain against the Anchorage Digital Address Signing Root CA (hard-coded by the client).

V1 address signature verification

Steps

  1. Verify the signature:
    • Decode addressSignaturePayload from hex to bytes.
    • Decode signature from hex to bytes.
    • Using your organization’s fixed public key, verify that signatureBytes is a valid Ed25519 signature of addressSignaturePayloadBytes.
  2. Verify the signed address matches the address to be used:
    • Decode addressSignaturePayload from hex to bytes.
    • Parse the bytes as JSON.
    • Verify the address matches the TextAddress property in the JSON.
Validating the signature alone is insufficient. You must also confirm that TextAddress in the decoded payload matches the address you intend to use.

V1 signed payload fields

FieldDescription
TextAddressThe 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 with signatureVersion: V2 include a certChain field containing an X.509 certificate chain in PEM format.

Steps

  1. Verify the certificate chain:
    • Parse certChain as PEM-encoded X.509 certificates.
    • Verify the chain from the leaf to the trusted Anchorage Digital Root CA.
    • Verify all certificates are temporally valid (both notAfter and notBefore).
    • Verify the leaf certificate’s Subject Alternative Names include address-provider.anchorage.internal.
    • Verify the leaf certificate’s KeyUsage includes both digitalSignature and nonRepudiation (also known as contentCommitment).
    • Extract the public key from the leaf certificate. Currently only Ed25519 keys are supported, but this may change.
    The leaf certificate is at index 0, followed by zero or more intermediates. The root cert is excluded from the response. The number of certificates in the chain may change. Follow standard X.509 verification procedures — not all libraries perform all checks by default.
  2. Verify the signature:
    • Decode addressSignaturePayload from hex to bytes.
    • Decode signature from hex to bytes.
    • Using the leaf certificate’s public key, verify signatureBytes is a valid signature of the payload bytes.
  3. Verify the signed details:
    • Parse addressSignaturePayload bytes as JSON.
    • Verify SignatureExpiresAt ≥ current UTC Unix timestamp.
    • Verify TextAddress matches the address to be used.
    • Verify VaultId matches your expected Vault ID.
    • Verify NetworkId matches the expected network for this address.
    NetworkName is included for human readability and does not need to be verified. Do not use a “strict” JSON parser that rejects extra properties — future versions may add fields.
    Anchorage Digital periodically refreshes V2 signatures and the Address Signing Root CA before expiration. The deposit address itself will not change — only the signature, certificate chain, and Root CA are updated.

V2 signed payload fields

FieldDescription
TextAddressThe text format of the on-chain address.
VaultIdIdentifies the vault this address belongs to.
NetworkIdIdentifies the network this address can receive deposits on.
NetworkNameHuman-readable version of NetworkId.
SignatureExpiresAtUnix timestamp after which the signature must not be trusted.
{
  "VaultId": "dae6089e7c0836705f0562af0f1e4e1f",
  "TextAddress": "bcrt1q709skemgf5skpsnysvgme2s3ztehkutl390yl0wp29lnmum5uw7qg0qrwm",
  "NetworkName": "Bitcoin Regnet",
  "NetworkId": "BTC_R",
  "SignatureExpiresAt": 1769450713
}

Anchorage Digital Address Signing Root CAs

Hard-code the appropriate Root CA for the environment you are targeting. This value must be tamper-proof.
-----BEGIN CERTIFICATE-----
MIIBXTCCAQ+gAwIBAgIUQZI+MSvYTXQHra+3OAKnwAMzotUwBQYDK2VwMCAxHjAc
BgNVBAMMFWNhLmFuY2hvcmFnZS5pbnRlcm5hbDAeFw0yNjAxMjYwMDAwMDBaFw0y
NzAxMjYwMDAwMDBaMCAxHjAcBgNVBAMMFWNhLmFuY2hvcmFnZS5pbnRlcm5hbDAq
MAUGAytlcAMhADTh1nctgIHtAKNW8ww/bY606pJ3OP2dyZYcQrU2kG5jo1swWTAP
BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwICBDAUBgorBgEEAYaNHwEBBAYW
BHJvb3QwIAYDVR0RBBkwF4IVY2EuYW5jaG9yYWdlLmludGVybmFsMAUGAytlcANB
ANkkdudEjH9RTKbRAxrRXyMSS/TgmdSrAVYOZzoRDJlyc+5oD+a0pmmwWVe86xZi
37YbN1GzVlXcJAPpV6ceEQU=
-----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+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.")
}