blob: bb922892620610ab149d8fef6b8c0a7956d8bb81 [file] [log] [blame]
Lorenz Brunae0d90d2019-09-05 17:53:56 +02001// Copyright 2020 The Monogon Project Authors.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17package tpm
18
19import (
Lorenz Brunaa6b7342019-12-12 02:55:02 +010020 "bytes"
21 "crypto"
Lorenz Brunae0d90d2019-09-05 17:53:56 +020022 "crypto/rand"
Lorenz Brunaa6b7342019-12-12 02:55:02 +010023 "crypto/rsa"
24 "crypto/x509"
Lorenz Brunae0d90d2019-09-05 17:53:56 +020025 "fmt"
26 "io"
27 "os"
28 "path/filepath"
Lorenz Brunae0d90d2019-09-05 17:53:56 +020029 "strconv"
30 "sync"
Lorenz Brunaa6b7342019-12-12 02:55:02 +010031 "time"
32
33 "git.monogon.dev/source/nexantic.git/core/pkg/sysfs"
Lorenz Brunae0d90d2019-09-05 17:53:56 +020034
35 "github.com/gogo/protobuf/proto"
36 tpmpb "github.com/google/go-tpm-tools/proto"
37 "github.com/google/go-tpm-tools/tpm2tools"
38 "github.com/google/go-tpm/tpm2"
Lorenz Brunaa6b7342019-12-12 02:55:02 +010039 "github.com/google/go-tpm/tpmutil"
Lorenz Brunae0d90d2019-09-05 17:53:56 +020040 "github.com/pkg/errors"
41 "go.uber.org/zap"
42 "golang.org/x/sys/unix"
43)
44
45var (
Leopold Schabel68c58752019-11-14 21:00:59 +010046 // SecureBootPCRs are all PCRs that measure the current Secure Boot configuration.
47 // This is what we want if we rely on secure boot to verify boot integrity. The firmware
48 // hashes the secure boot policy and custom keys into the PCR.
49 //
50 // This requires an extra step that provisions the custom keys.
51 //
52 // Some background: https://mjg59.dreamwidth.org/48897.html?thread=1847297
53 // (the initramfs issue mentioned in the article has been solved by integrating
54 // it into the kernel binary, and we don't have a shim bootloader)
55 //
56 // PCR7 alone is not sufficient - it needs to be combined with firmware measurements.
Lorenz Brunae0d90d2019-09-05 17:53:56 +020057 SecureBootPCRs = []int{7}
58
59 // FirmwarePCRs are alle PCRs that contain the firmware measurements
60 // See https://trustedcomputinggroup.org/wp-content/uploads/TCG_EFI_Platform_1_22_Final_-v15.pdf
Leopold Schabel68c58752019-11-14 21:00:59 +010061 FirmwarePCRs = []int{
Lorenz Brunaa6b7342019-12-12 02:55:02 +010062 0, // platform firmware
63 2, // option ROM code
64 3, // option ROM configuration and data
Leopold Schabel68c58752019-11-14 21:00:59 +010065 }
Lorenz Brunae0d90d2019-09-05 17:53:56 +020066
Leopold Schabel68c58752019-11-14 21:00:59 +010067 // FullSystemPCRs are all PCRs that contain any measurements up to the currently running EFI payload.
68 FullSystemPCRs = []int{
Lorenz Brunaa6b7342019-12-12 02:55:02 +010069 0, // platform firmware
70 1, // host platform configuration
71 2, // option ROM code
72 3, // option ROM configuration and data
73 4, // EFI payload
Leopold Schabel68c58752019-11-14 21:00:59 +010074 }
75
76 // Using FullSystemPCRs is the most secure, but also the most brittle option since updating the EFI
77 // binary, updating the platform firmware, changing platform settings or updating the binary
78 // would invalidate the sealed data. It's annoying (but possible) to predict values for PCR4,
79 // and even more annoying for the firmware PCR (comparison to known values on similar hardware
80 // is the only thing that comes to mind).
81 //
82 // See also: https://github.com/mxre/sealkey (generates PCR4 from EFI image, BSD license)
83 //
84 // Using only SecureBootPCRs is the easiest and still reasonably secure, if we assume that the
85 // platform knows how to take care of itself (i.e. Intel Boot Guard), and that secure boot
86 // is implemented properly. It is, however, a much larger amount of code we need to trust.
87 //
88 // We do not care about PCR 5 (GPT partition table) since modifying it is harmless. All of
89 // the boot options and cmdline are hardcoded in the kernel image, and we use no bootloader,
90 // so there's no PCR for bootloader configuration or kernel cmdline.
Lorenz Brunae0d90d2019-09-05 17:53:56 +020091)
92
93var (
Lorenz Brunaa6b7342019-12-12 02:55:02 +010094 numSRTMPCRs = 16
95 srtmPCRs = tpm2.PCRSelection{Hash: tpm2.AlgSHA256, PCRs: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}}
96 // TCG Trusted Platform Module Library Level 00 Revision 0.99 Table 6
97 tpmGeneratedValue = uint32(0xff544347)
98)
99
100var (
Lorenz Brunae0d90d2019-09-05 17:53:56 +0200101 // ErrNotExists is returned when no TPMs are available in the system
102 ErrNotExists = errors.New("no TPMs found")
103 // ErrNotInitialized is returned when this package was not initialized successfully
104 ErrNotInitialized = errors.New("no TPM was initialized")
105)
106
107// Singleton since the TPM is too
108var tpm *TPM
109
110// We're serializing all TPM operations since it has a limited number of handles and recovering
111// if it runs out is difficult to implement correctly. Might also be marginally more secure.
112var lock sync.Mutex
113
114// TPM represents a high-level interface to a connected TPM 2.0
115type TPM struct {
116 logger *zap.Logger
117 device io.ReadWriteCloser
Lorenz Brunaa6b7342019-12-12 02:55:02 +0100118
119 // We keep the AK loaded since it's used fairly often and deriving it is expensive
120 akHandleCache tpmutil.Handle
121 akPublicKey crypto.PublicKey
Lorenz Brunae0d90d2019-09-05 17:53:56 +0200122}
123
124// Initialize finds and opens the TPM (if any). If there is no TPM available it returns
125// ErrNotExists
126func Initialize(logger *zap.Logger) error {
127 lock.Lock()
128 defer lock.Unlock()
129 tpmDir, err := os.Open("/sys/class/tpm")
130 if err != nil {
131 return errors.Wrap(err, "failed to open sysfs TPM class")
132 }
133 defer tpmDir.Close()
134
135 tpms, err := tpmDir.Readdirnames(2)
136 if err != nil {
137 return errors.Wrap(err, "failed to read TPM device class")
138 }
139
140 if len(tpms) == 0 {
141 return ErrNotExists
142 }
143 if len(tpms) > 1 {
144 logger.Warn("Found more than one TPM, using the first one")
145 }
146 tpmName := tpms[0]
147 ueventData, err := sysfs.ReadUevents(filepath.Join("/sys/class/tpm", tpmName, "uevent"))
148 majorDev, err := strconv.Atoi(ueventData["MAJOR"])
149 if err != nil {
150 return fmt.Errorf("failed to convert uevent: %w", err)
151 }
152 minorDev, err := strconv.Atoi(ueventData["MINOR"])
153 if err != nil {
154 return fmt.Errorf("failed to convert uevent: %w", err)
155 }
156 if err := unix.Mknod("/dev/tpm", 0600|unix.S_IFCHR, int(unix.Mkdev(uint32(majorDev), uint32(minorDev)))); err != nil {
157 return errors.Wrap(err, "failed to create TPM device node")
158 }
159 device, err := tpm2.OpenTPM("/dev/tpm")
160 if err != nil {
161 return errors.Wrap(err, "failed to open TPM")
162 }
163 tpm = &TPM{
164 device: device,
165 logger: logger,
166 }
167 return nil
168}
169
170// GenerateSafeKey uses two sources of randomness (Kernel & TPM) to generate the key
171func GenerateSafeKey(size uint16) ([]byte, error) {
172 lock.Lock()
173 defer lock.Unlock()
174 if tpm == nil {
175 return []byte{}, ErrNotInitialized
176 }
177 encryptionKeyHost := make([]byte, size)
178 if _, err := io.ReadFull(rand.Reader, encryptionKeyHost); err != nil {
179 return []byte{}, errors.Wrap(err, "failed to generate host portion of new key")
180 }
181 var encryptionKeyTPM []byte
182 for i := 48; i > 0; i-- {
183 tpmKeyPart, err := tpm2.GetRandom(tpm.device, size-uint16(len(encryptionKeyTPM)))
184 if err != nil {
185 return []byte{}, errors.Wrap(err, "failed to generate TPM portion of new key")
186 }
187 encryptionKeyTPM = append(encryptionKeyTPM, tpmKeyPart...)
188 if len(encryptionKeyTPM) >= int(size) {
189 break
190 }
191 }
192
193 if len(encryptionKeyTPM) != int(size) {
194 return []byte{}, fmt.Errorf("got incorrect amount of TPM randomess: %v, requested %v", len(encryptionKeyTPM), size)
195 }
196
197 encryptionKey := make([]byte, size)
198 for i := uint16(0); i < size; i++ {
199 encryptionKey[i] = encryptionKeyHost[i] ^ encryptionKeyTPM[i]
200 }
201 return encryptionKey, nil
202}
203
204// Seal seals sensitive data and only allows access if the current platform configuration in
205// matches the one the data was sealed on.
206func Seal(data []byte, pcrs []int) ([]byte, error) {
207 lock.Lock()
208 defer lock.Unlock()
209 if tpm == nil {
210 return []byte{}, ErrNotInitialized
211 }
212 srk, err := tpm2tools.StorageRootKeyRSA(tpm.device)
213 if err != nil {
214 return []byte{}, errors.Wrap(err, "failed to load TPM SRK")
215 }
216 defer srk.Close()
217 sealedKey, err := srk.Seal(pcrs, data)
218 sealedKeyRaw, err := proto.Marshal(sealedKey)
219 if err != nil {
220 return []byte{}, errors.Wrapf(err, "failed to marshal sealed data")
221 }
222 return sealedKeyRaw, nil
223}
224
225// Unseal unseals sensitive data if the current platform configuration allows and sealing constraints
226// allow it.
227func Unseal(data []byte) ([]byte, error) {
228 lock.Lock()
229 defer lock.Unlock()
230 if tpm == nil {
231 return []byte{}, ErrNotInitialized
232 }
233 srk, err := tpm2tools.StorageRootKeyRSA(tpm.device)
234 if err != nil {
235 return []byte{}, errors.Wrap(err, "failed to load TPM SRK")
236 }
237 defer srk.Close()
238
239 var sealedKey tpmpb.SealedBytes
240 if err := proto.Unmarshal(data, &sealedKey); err != nil {
241 return []byte{}, errors.Wrap(err, "failed to decode sealed data")
242 }
243 // Logging this for auditing purposes
244 tpm.logger.Info("Attempting to unseal data protected with PCRs", zap.Int32s("pcrs", sealedKey.Pcrs))
245 unsealedData, err := srk.Unseal(&sealedKey)
246 if err != nil {
247 return []byte{}, errors.Wrap(err, "failed to unseal data")
248 }
249 return unsealedData, nil
250}
Lorenz Brunaa6b7342019-12-12 02:55:02 +0100251
252// Standard AK template for RSA2048 non-duplicatable restricted signing for attestation
253var akTemplate = tpm2.Public{
254 Type: tpm2.AlgRSA,
255 NameAlg: tpm2.AlgSHA256,
256 Attributes: tpm2.FlagSignerDefault,
257 RSAParameters: &tpm2.RSAParams{
258 Sign: &tpm2.SigScheme{
259 Alg: tpm2.AlgRSASSA,
260 Hash: tpm2.AlgSHA256,
261 },
262 KeyBits: 2048,
263 },
264}
265
266func loadAK() error {
267 var err error
268 // Rationale: The AK is a EK-equivalent key and used only for attestation. Using a non-primary
269 // key here would require us to store the wrapped version somewhere, which is inconvenient.
270 // This being a primary key in the Endorsement hierarchy means that it can always be recreated
271 // and can never be "destroyed". Under our security model this is of no concern since we identify
272 // a node by its IK (Identity Key) which we can destroy.
273 tpm.akHandleCache, tpm.akPublicKey, err = tpm2.CreatePrimary(tpm.device, tpm2.HandleEndorsement,
274 tpm2.PCRSelection{}, "", "", akTemplate)
275 return err
276}
277
278// Process documented in TCG EK Credential Profile 2.2.1
279func loadEK() (tpmutil.Handle, crypto.PublicKey, error) {
280 // The EK is a primary key which is supposed to be certified by the manufacturer of the TPM.
281 // Its public attributes are standardized in TCG EK Credential Profile 2.0 Table 1. These need
282 // to match exactly or we aren't getting the key the manufacturere signed. tpm2tools contains
283 // such a template already, so we're using that instead of redoing it ourselves.
284 // This ignores the more complicated ways EKs can be specified, the additional stuff you can do
285 // is just absolutely crazy (see 2.2.1.2 onward)
286 return tpm2.CreatePrimary(tpm.device, tpm2.HandleEndorsement,
287 tpm2.PCRSelection{}, "", "", tpm2tools.DefaultEKTemplateRSA())
288}
289
290// GetAKPublic gets the TPM2T_PUBLIC of the AK key
291func GetAKPublic() ([]byte, error) {
292 lock.Lock()
293 defer lock.Unlock()
294 if tpm == nil {
295 return []byte{}, ErrNotInitialized
296 }
297 if tpm.akHandleCache == tpmutil.Handle(0) {
298 if err := loadAK(); err != nil {
299 return []byte{}, fmt.Errorf("failed to load AK primary key: %w", err)
300 }
301 }
302 public, _, _, err := tpm2.ReadPublic(tpm.device, tpm.akHandleCache)
303 if err != nil {
304 return []byte{}, err
305 }
306 return public.Encode()
307}
308
309// TCG TPM v2.0 Provisioning Guidance v1.0 7.8 Table 2 and
310// TCG EK Credential Profile v2.1 2.2.1.4 de-facto Standard for Windows
311// These are both non-normative and reference Windows 10 documentation that's no longer available :(
312// But in practice this is what people are using, so if it's normative or not doesn't really matter
313const ekCertHandle = 0x01c00002
314
315// GetEKPublic gets the public key and (if available) Certificate of the EK
316func GetEKPublic() ([]byte, []byte, error) {
317 lock.Lock()
318 defer lock.Unlock()
319 if tpm == nil {
320 return []byte{}, []byte{}, ErrNotInitialized
321 }
322 ekHandle, publicRaw, err := loadEK()
323 if err != nil {
324 return []byte{}, []byte{}, fmt.Errorf("failed to load EK primary key: %w", err)
325 }
326 defer tpm2.FlushContext(tpm.device, ekHandle)
327 // Don't question the use of HandleOwner, that's the Standardâ„¢
328 ekCertRaw, err := tpm2.NVReadEx(tpm.device, ekCertHandle, tpm2.HandleOwner, "", 0)
329 if err != nil {
330 return []byte{}, []byte{}, err
331 }
332
333 publicKey, err := x509.MarshalPKIXPublicKey(publicRaw)
334 if err != nil {
335 return []byte{}, []byte{}, err
336 }
337
338 return publicKey, ekCertRaw, nil
339}
340
341// MakeAKChallenge generates a challenge for TPM residency and attributes of the AK
342func MakeAKChallenge(ekPubKey, akPub []byte, nonce []byte) ([]byte, []byte, error) {
343 ekPubKeyData, err := x509.ParsePKIXPublicKey(ekPubKey)
344 if err != nil {
345 return []byte{}, []byte{}, fmt.Errorf("failed to decode EK pubkey: %w", err)
346 }
347 akPubData, err := tpm2.DecodePublic(akPub)
348 if err != nil {
349 return []byte{}, []byte{}, fmt.Errorf("failed to decode AK public part: %w", err)
350 }
351 // Make sure we're attesting the right attributes (in particular Restricted)
352 if !akPubData.MatchesTemplate(akTemplate) {
353 return []byte{}, []byte{}, errors.New("the key being challenged is not a valid AK")
354 }
355 akName, err := akPubData.Name()
356 if err != nil {
357 return []byte{}, []byte{}, fmt.Errorf("failed to derive AK name: %w", err)
358 }
359 return generateRSA(akName.Digest, ekPubKeyData.(*rsa.PublicKey), 16, nonce, rand.Reader)
360}
361
362// SolveAKChallenge solves a challenge for TPM residency of the AK
363func SolveAKChallenge(credBlob, secretChallenge []byte) ([]byte, error) {
364 lock.Lock()
365 defer lock.Unlock()
366 if tpm == nil {
367 return []byte{}, ErrNotInitialized
368 }
369 if tpm.akHandleCache == tpmutil.Handle(0) {
370 if err := loadAK(); err != nil {
371 return []byte{}, fmt.Errorf("failed to load AK primary key: %w", err)
372 }
373 }
374
375 ekHandle, _, err := loadEK()
376 if err != nil {
377 return []byte{}, fmt.Errorf("failed to load EK: %w", err)
378 }
379 defer tpm2.FlushContext(tpm.device, ekHandle)
380
381 // This is necessary since the EK requires an endorsement handle policy in its session
382 // For us this is stupid because we keep all hierarchies open anyways since a) we cannot safely
383 // store secrets on the OS side pre-global unlock and b) it makes no sense in this security model
384 // since an uncompromised host OS will not let an untrusted entity attest as itself and a
385 // compromised OS can either not pass PCR policy checks or the game's already over (you
386 // successfully runtime-exploited a production Smalltown Core)
387 endorsementSession, _, err := tpm2.StartAuthSession(
388 tpm.device,
389 tpm2.HandleNull,
390 tpm2.HandleNull,
391 make([]byte, 16),
392 nil,
393 tpm2.SessionPolicy,
394 tpm2.AlgNull,
395 tpm2.AlgSHA256)
396 if err != nil {
397 panic(err)
398 }
399 defer tpm2.FlushContext(tpm.device, endorsementSession)
400
401 _, err = tpm2.PolicySecret(tpm.device, tpm2.HandleEndorsement, tpm2.AuthCommand{Session: tpm2.HandlePasswordSession, Attributes: tpm2.AttrContinueSession}, endorsementSession, nil, nil, nil, 0)
402 if err != nil {
403 return []byte{}, fmt.Errorf("failed to make a policy secret session: %w", err)
404 }
405
406 for {
407 solution, err := tpm2.ActivateCredentialUsingAuth(tpm.device, []tpm2.AuthCommand{
408 {Session: tpm2.HandlePasswordSession, Attributes: tpm2.AttrContinueSession}, // Use standard no-password authentication
409 {Session: endorsementSession, Attributes: tpm2.AttrContinueSession}, // Use a full policy session for the EK
410 }, tpm.akHandleCache, ekHandle, credBlob, secretChallenge)
411 if warn, ok := err.(tpm2.Warning); ok && warn.Code == tpm2.RCRetry {
412 time.Sleep(100 * time.Millisecond)
413 continue
414 }
415 return solution, err
416 }
417}
418
419// FlushTransientHandles flushes all sessions and non-persistent handles
420func FlushTransientHandles() error {
421 lock.Lock()
422 defer lock.Unlock()
423 if tpm == nil {
424 return ErrNotInitialized
425 }
426 flushHandleTypes := []tpm2.HandleType{tpm2.HandleTypeTransient, tpm2.HandleTypeLoadedSession, tpm2.HandleTypeSavedSession}
427 for _, handleType := range flushHandleTypes {
428 handles, err := tpm2tools.Handles(tpm.device, handleType)
429 if err != nil {
430 return err
431 }
432 for _, handle := range handles {
433 if err := tpm2.FlushContext(tpm.device, handle); err != nil {
434 return err
435 }
436 }
437 }
438 return nil
439}
440
441// AttestPlatform performs a PCR quote using the AK and returns the quote and its signature
442func AttestPlatform(nonce []byte) ([]byte, []byte, error) {
443 lock.Lock()
444 defer lock.Unlock()
445 if tpm == nil {
446 return []byte{}, []byte{}, ErrNotInitialized
447 }
448 if tpm.akHandleCache == tpmutil.Handle(0) {
449 if err := loadAK(); err != nil {
450 return []byte{}, []byte{}, fmt.Errorf("failed to load AK primary key: %w", err)
451 }
452 }
453 // We only care about SHA256 since SHA1 is weak. This is supported on at least GCE and
454 // Intel / AMD fTPM, which is good enough for now. Alg is null because that would just hash the
455 // nonce, which is dumb.
456 quote, signature, err := tpm2.Quote(tpm.device, tpm.akHandleCache, "", "", nonce, srtmPCRs,
457 tpm2.AlgNull)
458 if err != nil {
459 return []byte{}, []byte{}, fmt.Errorf("failed to quote PCRs: %w", err)
460 }
461 return quote, signature.RSA.Signature, err
462}
463
464// VerifyAttestPlatform verifies a given attestation. You can rely on all data coming back as being
465// from the TPM on which the AK is bound to.
466func VerifyAttestPlatform(nonce, akPub, quote, signature []byte) (*tpm2.AttestationData, error) {
467 hash := crypto.SHA256.New()
468 hash.Write(quote)
469
470 akPubData, err := tpm2.DecodePublic(akPub)
471 if err != nil {
472 return nil, fmt.Errorf("invalid AK: %w", err)
473 }
474 akPublicKey, err := akPubData.Key()
475 if err != nil {
476 return nil, fmt.Errorf("invalid AK: %w", err)
477 }
478 akRSAKey, ok := akPublicKey.(*rsa.PublicKey)
479 if !ok {
480 return nil, errors.New("invalid AK: invalid key type")
481 }
482
483 if err := rsa.VerifyPKCS1v15(akRSAKey, crypto.SHA256, hash.Sum(nil), signature); err != nil {
484 return nil, err
485 }
486
487 quoteData, err := tpm2.DecodeAttestationData(quote)
488 if err != nil {
489 return nil, err
490 }
491 // quoteData.Magic works together with the TPM's Restricted key attribute. If this attribute is set
492 // (which it needs to be for the AK to be considered valid) the TPM will not sign external data
493 // having this prefix with such a key. Only data that originates inside the TPM like quotes and
494 // key certifications can have this prefix and sill be signed by a restricted key. This check
495 // is thus vital, otherwise somebody can just feed the TPM an arbitrary attestation to sign with
496 // its AK and this function will happily accept the forged attestation.
497 if quoteData.Magic != tpmGeneratedValue {
498 return nil, errors.New("invalid TPM quote: data marker for internal data not set - forged attestation")
499 }
500 if quoteData.Type != tpm2.TagAttestQuote {
501 return nil, errors.New("invalid TPM qoute: not a TPM quote")
502 }
503 if !bytes.Equal(quoteData.ExtraData, nonce) {
504 return nil, errors.New("invalid TPM quote: wrong nonce")
505 }
506
507 return quoteData, nil
508}
509
510// GetPCRs returns all SRTM PCRs in-order
511func GetPCRs() ([][]byte, error) {
512 lock.Lock()
513 defer lock.Unlock()
514 if tpm == nil {
515 return [][]byte{}, ErrNotInitialized
516 }
517 pcrs := make([][]byte, numSRTMPCRs)
518
519 // The TPM can (and most do) return partial results. Let's just retry as many times as we have
520 // PCRs since each read should return at least one PCR.
521readLoop:
522 for i := 0; i < numSRTMPCRs; i++ {
523 sel := tpm2.PCRSelection{Hash: tpm2.AlgSHA256}
524 for pcrN := 0; pcrN < numSRTMPCRs; pcrN++ {
525 if len(pcrs[pcrN]) == 0 {
526 sel.PCRs = append(sel.PCRs, pcrN)
527 }
528 }
529
530 readPCRs, err := tpm2.ReadPCRs(tpm.device, sel)
531 if err != nil {
532 return nil, fmt.Errorf("failed to read PCRs: %w", err)
533 }
534
535 for pcrN, pcr := range readPCRs {
536 pcrs[pcrN] = pcr
537 }
538 for _, pcr := range pcrs {
539 // If at least one PCR is still not read, continue
540 if len(pcr) == 0 {
541 continue readLoop
542 }
543 }
544 break
545 }
546
547 return pcrs, nil
548}