Files
zeekay 0d0faff0f8 merge: ARM CCA realm verifier (feat/kind-armcca) — 6th vendor, converged
ARM CCA Realm attestation: a CBOR/COSE CCA token collection (platform token
signed by the operator-pinned CPAK/IAK + realm token signed by the RAK, with
the H(RAK) platform-realm binding enforced). Vendor arm.cca, fail-closed zero
value, real COSE_Sign1 verification.

armcca branched before the registry landed and carried its own parallel copy
(extraVerifiers map + a duplicate registerVerifier + a second VerifierFor
lookup). Converged to the one registry: deleted that block so armcca.go's
init() registers through the canonical registerVerifier in verifier.go, and
repointed its self-registration test to RegisteredVerifier. There is one
registry, one lookup, one dispatch path — no second way.

go.mod: unioned the require blocks (cbor/v2, go-sev-guest, go-tdx-guest,
veraison/ccatoken, go-cose); go mod tidy clean; ccatoken is v1.4.0 and its
transitive jwx/v2, secp256k1/v4 carry their major in the import path — no dep
bumped to a new major. doc.go + verifier.go: ARM CCA is now a real 6th Kind,
not pending. verifier_test.go: TestRegistry_RoutesEveryKind covers all six.

go build/vet/test + go test -race ./attest/... green; gofmt clean.
2026-06-27 14:23:58 -07:00

579 lines
21 KiB
Go

// Copyright (c) 2026, Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause
package attest
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/sha256"
"crypto/sha512"
"crypto/subtle"
"encoding/hex"
"errors"
"fmt"
"hash"
cbor "github.com/fxamacker/cbor/v2"
cose "github.com/veraison/go-cose"
)
// KindARMCCA tags ARM Confidential Compute Architecture (CCA) Realm
// attestation evidence: a CBOR/COSE attestation-token collection produced by
// the Realm Management Monitor (RMM) under the Realm Management Extension
// (RME). See the ARMCCA verifier below for the wire shape and trust model.
const KindARMCCA Kind = "arm_cca"
// Top-level CBOR tags that frame a CCA attestation-token collection. A CCA
// token bundles two COSE_Sign1 tokens — the CCA platform token and the realm
// token — under one of these tags. Both are accepted; they differ only in the
// envelope, never in the inner tokens.
const (
// tagCCACMW is the current CMW collection framing
// (draft-ietf-rats-msg-wrap). Each map value is a CoAP-content-format-
// tagged [int, bstr] pair whose bstr is the COSE_Sign1.
tagCCACMW uint64 = 907
// tagCCAEAT is the legacy EAT collection framing. Each map value is the
// COSE_Sign1 bstr directly. Retained because deployed RMM firmware (e.g.
// Trusted Firmware) still emits it.
tagCCAEAT uint64 = 399
)
// CCA token CWT claim labels. The realm and platform tokens are CBOR Web
// Tokens whose claims are keyed by these integer labels (ARM CCA token format,
// aligned with the Veraison CCA reference). The challenge (10) and profile
// (265) labels are shared by both tokens; the rest are token-specific.
const (
labelChallenge int64 = 10 // CWT nonce: platform binder / realm freshness
labelProfile int64 = 265 // EAT profile; presence selects the RAK encoding
// CCA realm token claims.
labelRealmInitialMeasurement int64 = 44238 // RIM
labelRealmExtensibleMeas int64 = 44239 // REM[]
labelRealmHashAlgID int64 = 44236
labelRealmPubKey int64 = 44237 // RAK
labelRealmPubKeyHashAlgID int64 = 44240 // binding hash algorithm
// CCA platform token claims.
labelPlatformInstID int64 = 256
labelPlatformConfig int64 = 2401
labelPlatformLifeCycle int64 = 2395
)
// ARMCCA verifies an ARM CCA Realm attestation-token collection per the IETF
// RATS model and the ARM CCA token format.
//
// # Wire shape
//
// collection (CBOR tag 907 CMW, or tag 399 legacy EAT)
// ├─ 44234 → CCA platform token (COSE_Sign1, signed by the CPAK)
// └─ 44241 → CCA realm token (COSE_Sign1, signed by the RAK)
//
// The platform token is a CWT describing the platform (monitor/RMM + root
// world), signed by the silicon-provisioned CCA Platform Attestation Key
// (CPAK, a.k.a. IAK). The realm token is a CWT describing one Realm, signed by
// a per-Realm Realm Attestation Key (RAK) that the RMM generates and embeds in
// the realm claims. The platform token's challenge claim binds the two: it
// equals H(RAK), so a CPAK-rooted platform vouches for exactly this realm's
// signing key.
//
// # What Verify checks
//
// 1. The platform token COSE_Sign1 signature, against an operator-pinned CPAK
// in TrustAnchors. (cryptographic chain to the ARM CCA root of trust)
// 2. The realm token COSE_Sign1 signature, against the RAK extracted from the
// realm claims.
// 3. The platform↔realm binding: platform nonce == H_alg(RAK), where alg is
// the realm's declared public-key hash algorithm. (a forged or mismatched
// realm token is refused even when both signatures are individually valid)
// 4. The realm initial measurement (RIM) and extensible measurements (REM),
// and the CCA platform config claim.
// 5. Freshness: the realm challenge claim is the relying party's nonce; pin
// it with WithExpectedReportData. Pin the RIM with WithExpectedMeasurement.
//
// On success the RIM lands in VerifiedReport.Measurement, the realm challenge
// in ReportData, and the platform instance-id in ChipID. The REMs, config,
// lifecycle, and profiles land in Extra.
//
// # Implementation
//
// Built on exactly the two primitives of the Veraison/ARM CCA stack: CBOR
// (github.com/fxamacker/cbor/v2) for the collection envelope and CWT claim
// maps, and COSE (github.com/veraison/go-cose) for COSE_Sign1 signature
// verification and the COSE_Key realm-key decode. No higher-level token
// library is pulled into the trusted path; the claim labels are pinned here
// and cross-checked against the canonical reference in the test suite.
//
// # Trust model (the honest production scope)
//
// Unlike AMD SEV-SNP — where go-sev-guest embeds AMD's ARK and fetches the
// VCEK chain from the AMD KDS — there is NO universal ARM root baked into any
// library. The CPAK is provisioned per-SoC by the silicon vendor and endorsed
// out-of-band (a CCA platform endorsement / CoRIM from the vendor, or the
// platform's Hardware Enforced Security subsystem). The relying party MUST
// obtain the authentic CPAK public key through that supply chain and pin it in
// TrustAnchors. An empty TrustAnchors set is refused — there is no insecure
// mode, exactly as KindNVTrust refuses an empty trust-root set.
//
// This verifier proves the token is cryptographically authentic, internally
// consistent, bound, and fresh. It does NOT, on its own, judge whether the
// extracted RIM/REM/config correspond to a known-good Realm + platform image:
// that is reference-values policy (a CCA CoRIM from the Realm owner), supplied
// here as WithExpectedMeasurement or enforced by a matcher above this verifier.
// CPAK endorsement revocation is likewise an out-of-band concern. These are
// documented, not stubbed: the cryptographic verification is complete and real.
//
// TrustAnchors are construction-time configuration on the verifier value
// (the package centralises per-call policy options in verifier.go's config,
// which this Kind does not modify); per-call freshness/measurement policy
// still flows through the shared WithExpected* options.
type ARMCCA struct {
// TrustAnchors are the operator-pinned CCA platform attestation public
// keys (CPAK/IAK). The platform token must be signed by one of them.
// Empty ⇒ Verify refuses with ErrPolicy. ECDSA P-256/P-384 keys, as
// delivered by x509.ParsePKIXPublicKey, are the expected element type.
TrustAnchors []crypto.PublicKey
}
// Verify implements Verifier for ARM CCA Realm attestation tokens.
func (v ARMCCA) Verify(ctx context.Context, evidence []byte, opts ...Option) (*VerifiedReport, error) {
cfg := applyOptions(opts...)
// No insecure mode: a pinned CPAK is mandatory. Refuse rather than fall
// back to trusting whatever signed the platform token.
if len(v.TrustAnchors) == 0 {
return nil, fmt.Errorf("%w: arm cca requires pinned platform trust anchors (ARMCCA.TrustAnchors)", ErrPolicy)
}
// 1. Parse the collection envelope (CMW tag 907 or legacy EAT tag 399)
// into the two raw COSE_Sign1 tokens.
platformTok, realmTok, err := decodeCCACollection(evidence)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidEvidence, err)
}
// 2. Verify the platform token signature against a pinned CPAK. This is
// the chain to the ARM CCA root of trust.
platformSign1, err := decodeCOSESign1(platformTok)
if err != nil {
return nil, fmt.Errorf("%w: platform token: %v", ErrInvalidEvidence, err)
}
if err := verifyCOSESign1(platformSign1, v.TrustAnchors); err != nil {
return nil, fmt.Errorf("platform token: %w", err)
}
// 3. Decode the (now signature-verified) platform claims.
platformClaims, err := decodeClaimSet(platformSign1.Payload)
if err != nil {
return nil, fmt.Errorf("%w: platform claims: %v", ErrInvalidEvidence, err)
}
// 4. Decode the realm token + claims and extract the RAK. The RAK is
// self-asserted by the realm token; it only becomes trusted once the
// binding (step 6) ties it to the CPAK-rooted platform token.
realmSign1, err := decodeCOSESign1(realmTok)
if err != nil {
return nil, fmt.Errorf("%w: realm token: %v", ErrInvalidEvidence, err)
}
realmClaims, err := decodeClaimSet(realmSign1.Payload)
if err != nil {
return nil, fmt.Errorf("%w: realm claims: %v", ErrInvalidEvidence, err)
}
rak, err := extractRAK(realmClaims)
if err != nil {
return nil, fmt.Errorf("%w: realm public key: %v", ErrInvalidEvidence, err)
}
// 5. Verify the realm token signature against the RAK.
if err := verifyCOSESign1(realmSign1, []crypto.PublicKey{rak}); err != nil {
return nil, fmt.Errorf("realm token: %w", err)
}
// 6. Verify the platform↔realm binding: platform nonce == H_alg(RAK).
if err := checkCCABinding(platformClaims, realmClaims); err != nil {
return nil, err
}
// 7. Extract the measurements and the freshness nonce.
rim, ok := realmClaims.bytes(labelRealmInitialMeasurement)
if !ok {
return nil, fmt.Errorf("%w: realm initial measurement claim missing", ErrInvalidEvidence)
}
challenge, ok := realmClaims.bytes(labelChallenge)
if !ok {
return nil, fmt.Errorf("%w: realm challenge claim missing", ErrInvalidEvidence)
}
// 8. Caller policy on the verified fields.
if cfg.expectedReportData != nil {
if subtle.ConstantTimeCompare(cfg.expectedReportData, challenge) != 1 {
return nil, fmt.Errorf("%w: realm challenge mismatch (got %s)",
ErrPolicy, hex.EncodeToString(challenge))
}
}
if cfg.expectedMeasurement != nil {
if subtle.ConstantTimeCompare(cfg.expectedMeasurement, rim) != 1 {
return nil, fmt.Errorf("%w: realm initial measurement mismatch (got %s)",
ErrPolicy, hex.EncodeToString(rim))
}
}
instID, _ := platformClaims.bytes(labelPlatformInstID)
out := &VerifiedReport{
Kind: KindARMCCA,
Vendor: "arm.cca",
Measurement: cloneBytes(rim),
ReportData: cloneBytes(challenge),
ChipID: cloneBytes(instID),
IssuedAt: cfg.nowOrWall().UTC(),
Extra: buildCCAExtra(platformClaims, realmClaims),
}
out.CompositeHash = computeCompositeHash(KindARMCCA, canonicalCCABytes(platformClaims, realmClaims))
return out, nil
}
// ccaClaimSet is a decoded CWT claims map keyed by integer label. Values are
// kept raw so each is decoded into its concrete type only on demand.
type ccaClaimSet map[int64]cbor.RawMessage
// decodeClaimSet decodes a COSE_Sign1 payload (a CWT claims map) into a
// label-keyed claim set.
func decodeClaimSet(payload []byte) (ccaClaimSet, error) {
var m ccaClaimSet
if err := cbor.Unmarshal(payload, &m); err != nil {
return nil, err
}
if len(m) == 0 {
return nil, errors.New("empty claim set")
}
return m, nil
}
func (c ccaClaimSet) has(label int64) bool {
_, ok := c[label]
return ok
}
// bytes decodes the claim at label as a CBOR byte string. Returns false if the
// claim is absent or is not a byte string.
func (c ccaClaimSet) bytes(label int64) ([]byte, bool) {
raw, ok := c[label]
if !ok {
return nil, false
}
var b []byte
if err := cbor.Unmarshal(raw, &b); err != nil {
return nil, false
}
return b, true
}
// text decodes the claim at label as a CBOR text string.
func (c ccaClaimSet) text(label int64) (string, bool) {
raw, ok := c[label]
if !ok {
return "", false
}
var s string
if err := cbor.Unmarshal(raw, &s); err != nil {
return "", false
}
return s, true
}
// sliceBytes decodes the claim at label as a CBOR array of byte strings.
func (c ccaClaimSet) sliceBytes(label int64) ([][]byte, bool) {
raw, ok := c[label]
if !ok {
return nil, false
}
var s [][]byte
if err := cbor.Unmarshal(raw, &s); err != nil {
return nil, false
}
return s, true
}
// uintVal decodes the claim at label as a CBOR unsigned integer.
func (c ccaClaimSet) uintVal(label int64) (uint64, bool) {
raw, ok := c[label]
if !ok {
return 0, false
}
var u uint64
if err := cbor.Unmarshal(raw, &u); err != nil {
return 0, false
}
return u, true
}
// decodeCCACollection unwraps the CBOR collection (CMW tag 907 or legacy EAT
// tag 399) and returns the two inner COSE_Sign1 tokens. It tolerates no other
// top-level tag: the framing is supplied out-of-band by the Kind, so an
// unexpected tag is corruption, not an alternate format to sniff.
func decodeCCACollection(evidence []byte) (platformToken, realmToken []byte, err error) {
var tag cbor.RawTag
if err := tag.UnmarshalCBOR(evidence); err != nil {
return nil, nil, fmt.Errorf("not a tagged CCA collection: %w", err)
}
switch tag.Number {
case tagCCACMW:
// CMW: each label maps to [coap-content-format, token-bstr].
var c struct {
Platform cmwEntry `cbor:"44234,keyasint"`
Realm cmwEntry `cbor:"44241,keyasint"`
}
if err := cbor.Unmarshal(tag.Content, &c); err != nil {
return nil, nil, fmt.Errorf("decoding CMW collection: %w", err)
}
if len(c.Platform.Token) == 0 || len(c.Realm.Token) == 0 {
return nil, nil, errors.New("CMW collection missing platform or realm token")
}
return c.Platform.Token, c.Realm.Token, nil
case tagCCAEAT:
// Legacy EAT: each label maps directly to the token bstr.
var c struct {
Platform []byte `cbor:"44234,keyasint"`
Realm []byte `cbor:"44241,keyasint"`
}
if err := cbor.Unmarshal(tag.Content, &c); err != nil {
return nil, nil, fmt.Errorf("decoding EAT collection: %w", err)
}
if len(c.Platform) == 0 || len(c.Realm) == 0 {
return nil, nil, errors.New("EAT collection missing platform or realm token")
}
return c.Platform, c.Realm, nil
default:
return nil, nil, fmt.Errorf("unexpected top-level CBOR tag %d (want %d CMW or %d EAT)",
tag.Number, tagCCACMW, tagCCAEAT)
}
}
// cmwEntry is one CMW collection value: a [coap-content-format, token] array.
// The content format (263 = application/eat+cwt for these tokens) is parsed
// for completeness but not load-bearing; the inner bstr is the COSE_Sign1.
type cmwEntry struct {
_ struct{} `cbor:",toarray"`
CoapType int
Token []byte
}
// decodeCOSESign1 parses a tagged COSE_Sign1 (CBOR tag 18) token.
func decodeCOSESign1(token []byte) (*cose.Sign1Message, error) {
m := cose.NewSign1Message()
if err := m.UnmarshalCBOR(token); err != nil {
return nil, err
}
return m, nil
}
// verifyCOSESign1 verifies m's signature against the first candidate public
// key that both matches the token's declared COSE algorithm and validates the
// signature. CCA uses a zero-length external_aad. A token whose algorithm
// header is unreadable is ErrInvalidEvidence; a token no pinned key can verify
// is ErrSignatureInvalid.
func verifyCOSESign1(m *cose.Sign1Message, candidates []crypto.PublicKey) error {
alg, err := m.Headers.Protected.Algorithm()
if err != nil {
return fmt.Errorf("%w: unreadable COSE algorithm: %v", ErrInvalidEvidence, err)
}
var lastErr error
for _, key := range candidates {
verifier, verr := cose.NewVerifier(alg, key)
if verr != nil {
lastErr = verr
continue
}
if verr := m.Verify(nil, verifier); verr == nil {
return nil
} else {
lastErr = verr
}
}
return fmt.Errorf("%w: no pinned key verified the COSE_Sign1 (alg %v): %v",
ErrSignatureInvalid, alg, lastErr)
}
// extractRAK decodes the Realm Attestation Key from the realm claims. The
// encoding depends on the presence of a realm profile claim: profile present ⇒
// CBOR-encoded COSE_Key; profile absent (legacy RMM) ⇒ raw 0x04||X||Y P-384
// point. This mirrors the ARM CCA token format exactly.
func extractRAK(realmClaims ccaClaimSet) (*ecdsa.PublicKey, error) {
raw, ok := realmClaims.bytes(labelRealmPubKey)
if !ok {
return nil, errors.New("realm public key claim missing or malformed")
}
if realmClaims.has(labelProfile) {
return ecdsaFromCOSEKey(raw)
}
return ecdsaFromRawP384(raw)
}
// ecdsaFromRawP384 decodes a raw 0x04||X||Y uncompressed P-384 point. The
// stdlib unmarshal rejects points that are not on the curve, foreclosing
// invalid-curve attacks on the realm signature check.
func ecdsaFromRawP384(b []byte) (*ecdsa.PublicKey, error) {
x, y := elliptic.Unmarshal(elliptic.P384(), b) //nolint:staticcheck // uncompressed-point decode + on-curve check
if x == nil {
return nil, errors.New("realm public key is not a valid P-384 point")
}
return &ecdsa.PublicKey{Curve: elliptic.P384(), X: x, Y: y}, nil
}
// ecdsaFromCOSEKey decodes a CBOR COSE_Key (EC2) into an ECDSA public key.
func ecdsaFromCOSEKey(b []byte) (*ecdsa.PublicKey, error) {
var k cose.Key
if err := k.UnmarshalCBOR(b); err != nil {
return nil, err
}
if k.Type != cose.KeyTypeEC2 {
return nil, fmt.Errorf("realm COSE_Key is not EC2 (type %v)", k.Type)
}
pub, err := k.PublicKey()
if err != nil {
return nil, err
}
epk, ok := pub.(*ecdsa.PublicKey)
if !ok {
return nil, errors.New("realm COSE_Key is not an ECDSA public key")
}
return epk, nil
}
// checkCCABinding enforces the realm↔platform binding: the platform token's
// nonce claim must equal H_alg(RAK), where alg is the realm's declared
// public-key hash algorithm and RAK is the raw realm-public-key claim bytes.
// This is what makes a CPAK-rooted platform token vouch for THIS realm's
// signing key; without it, any valid realm token could be paired with any
// valid platform token. A mismatch is a broken chain (ErrChainInvalid).
func checkCCABinding(platformClaims, realmClaims ccaClaimSet) error {
rak, ok := realmClaims.bytes(labelRealmPubKey)
if !ok {
return fmt.Errorf("%w: realm public key missing for binding", ErrInvalidEvidence)
}
algID, ok := realmClaims.text(labelRealmPubKeyHashAlgID)
if !ok {
return fmt.Errorf("%w: realm public-key hash algorithm missing", ErrInvalidEvidence)
}
h, err := ccaHashByName(algID)
if err != nil {
return fmt.Errorf("%w: %v", ErrInvalidEvidence, err)
}
h.Write(rak)
binder := h.Sum(nil)
nonce, ok := platformClaims.bytes(labelChallenge)
if !ok {
return fmt.Errorf("%w: platform nonce claim missing", ErrInvalidEvidence)
}
if subtle.ConstantTimeCompare(binder, nonce) != 1 {
return fmt.Errorf("%w: platform nonce does not match H_%s(RAK)", ErrChainInvalid, algID)
}
return nil
}
// ccaHashByName maps a CCA hash-algorithm string to a fresh hash.Hash. The
// set matches the algorithms the ARM CCA token format admits for the RAK hash.
func ccaHashByName(name string) (hash.Hash, error) {
switch name {
case "sha-224":
return sha256.New224(), nil
case "sha-256":
return sha256.New(), nil
case "sha-384":
return sha512.New384(), nil
case "sha-512":
return sha512.New(), nil
default:
return nil, fmt.Errorf("unsupported CCA hash algorithm %q", name)
}
}
// buildCCAExtra collects CCA-specific fields that don't fit the common shape.
// Keys are stable wire identifiers prefixed "arm_cca.".
func buildCCAExtra(platformClaims, realmClaims ccaClaimSet) map[string]string {
extra := make(map[string]string, 10)
if p, ok := platformClaims.text(labelProfile); ok && p != "" {
extra["arm_cca.platform_profile"] = p
}
if p, ok := realmClaims.text(labelProfile); ok && p != "" {
extra["arm_cca.realm_profile"] = p
}
if cfg, ok := platformClaims.bytes(labelPlatformConfig); ok && len(cfg) > 0 {
extra["arm_cca.platform_config"] = hex.EncodeToString(cfg)
}
if lc, ok := platformClaims.uintVal(labelPlatformLifeCycle); ok {
extra["arm_cca.platform_lifecycle"] = fmt.Sprintf("%#04x", lc)
}
if alg, ok := realmClaims.text(labelRealmHashAlgID); ok && alg != "" {
extra["arm_cca.realm_hash_alg"] = alg
}
if alg, ok := realmClaims.text(labelRealmPubKeyHashAlgID); ok && alg != "" {
extra["arm_cca.rak_hash_alg"] = alg
}
if rems, ok := realmClaims.sliceBytes(labelRealmExtensibleMeas); ok {
extra["arm_cca.rem_count"] = fmt.Sprintf("%d", len(rems))
for i, rem := range rems {
extra[fmt.Sprintf("arm_cca.rem.%d", i)] = hex.EncodeToString(rem)
}
}
return extra
}
// canonicalCCABytes returns the bytes that participate in CompositeHash. The
// verifier-extracted fields are hashed (never the raw token) in a fixed,
// length-prefixed order, so a consumer re-deriving the hash reproduces it iff
// the same evidence was verified against the same trust anchor and bindings.
func canonicalCCABytes(platformClaims, realmClaims ccaClaimSet) []byte {
var buf []byte
appendField := func(b []byte) {
var l [4]byte
n := uint32(len(b))
l[0], l[1], l[2], l[3] = byte(n>>24), byte(n>>16), byte(n>>8), byte(n)
buf = append(buf, l[:]...)
buf = append(buf, b...)
}
field := func(c ccaClaimSet, label int64) []byte {
b, _ := c.bytes(label)
return b
}
textField := func(c ccaClaimSet, label int64) []byte {
s, _ := c.text(label)
return []byte(s)
}
appendField(textField(platformClaims, labelProfile))
appendField(textField(realmClaims, labelProfile))
appendField(field(platformClaims, labelPlatformConfig))
appendField(field(platformClaims, labelPlatformInstID))
appendField(field(platformClaims, labelChallenge)) // platform binder
appendField(field(realmClaims, labelRealmInitialMeasurement))
if rems, ok := realmClaims.sliceBytes(labelRealmExtensibleMeas); ok {
for _, rem := range rems {
appendField(rem)
}
}
appendField(field(realmClaims, labelChallenge)) // realm freshness nonce
appendField(field(realmClaims, labelRealmPubKey))
appendField(textField(realmClaims, labelRealmPubKeyHashAlgID))
appendField(textField(realmClaims, labelRealmHashAlgID))
return buf
}
// Self-registration. The registry itself (registerVerifier / RegisteredVerifier
// / Dispatch) is framework, in verifier.go; this Kind installs itself with one
// init() line, the same as every other Kind. ARM CCA branched before the
// registry landed and carried its own copy; that copy is gone — there is one
// registry, one lookup (RegisteredVerifier), one dispatch path.
func init() { registerVerifier(KindARMCCA, ARMCCA{}) }
// Compile-time guard: ARMCCA satisfies Verifier.
var _ Verifier = ARMCCA{}