certificates icon indicating copy to clipboard operation
certificates copied to clipboard

ACME: Support device-attest-01 challenge type

Open elyscape opened this issue 2 years ago • 2 comments

Hello!

  • Vote on this issue by adding a 👍 reaction
  • If you want to implement this feature, comment to let us know (we'll work with you on design, scheduling, etc.)

Issue details

Support for the the device-attest-01 ACME challenge type.

Why is this needed?

The device-attest-01 challenge type allows for certificates to be issued based on a signed attestation that the key is bound to a particular hardware device. This effectively extends ACME to be able to act as the root of an enterprise PKI.

To my knowledge, device-attest-01 is currently only supported by iOS/iPadOS/tvOS 16. However, the draft spec was written by a Google employee (@brandonweeks) and links to information on the attestation APIs for Android, Chrome OS, and TPMs; it therefore seems likely that support in other mobile operating systems is forthcoming.

elyscape avatar Jun 15 '22 00:06 elyscape

As a fun side bonus, it looks like @brandonweeks implemented a demo server in a fork of step-ca (server, client and config).

elyscape avatar Jun 15 '22 00:06 elyscape

Hey @elyscape, thanks for opening this issue! We definitely want to add this quickly - will reach out to brandon and see how much of the existing work we can use directly. We need to prioritize against existing projects, but expect an update soon :]

dopey avatar Jun 17 '22 18:06 dopey

Hi @dopey, @elyscape , I'm curious what's the status of this feature as I see some work was merged with this PR although testing it fails for me. Running step 0.23.0 locally, server responds to HTTP-01 challenge but fails device-attest-01 sent from iOS 16 and Ventura 13.1 beta 3, fails with: failed to request authorization which can be tracked to Apple code in here. please let me know if opening a Github issue or discussing this in Discord is more convenient.

taher-mosbah avatar Nov 17 '22 08:11 taher-mosbah

@taher-mosbah I think you may have to set the device-attest-01 challenge to enabled in your ACME provisioner. It'll look similar to this:

    {
        "type": "ACME",
        "name": "attestation",
        "challenges": ["device-attest-01"]
    }

This will only respond to ACME requests for which the device-attest-01 challenge would be sent, so it will fail for domain names, for example. By default, all existing challenges will be enabled. You can enable specific ones by adding them to the list: "challenges": ["dns-01", "http-01", "tls-alpn-01", "device-attest-01"]. We're working on some new docs that explain the ACME device-attest-01 challenge, when to use it and how to use it.

Edit: I checked the code and we might want to add a log to indicate to the CA operator that the request could not be served because no challenge type was enabled for the specific type of identifier. Currently there's no indication whatsoever.

hslatman avatar Nov 17 '22 08:11 hslatman

thanks @hslatman I wasn't aware of this, I'm having a runtime error now though with this config:

			{
				"type": "ACME",
				"name": "acme",
				"challenges": ["device-attest-01"],
				"claims": {
					"enableSSHCA": true,
					"disableRenewal": false,
					"allowRenewalAfterExpiry": false
				},
				"options": {
					"x509": {},
					"ssh": {}
				}
			}
2022/11/17 09:21:05 /usr/local/Cellar/go/1.19.3/libexec/src/net/http/server.go:3228: http: panic serving [fe80::58d8:12ff:fe1b:9054%en9]:64995: runtime error: invalid memory address or nil pointer dereference
goroutine 255 [running]:
net/http.(*conn).serve.func1()
	/usr/local/Cellar/go/1.19.3/libexec/src/net/http/server.go:1850 +0xbf
panic({0x2062100, 0x303e580})
	/usr/local/Cellar/go/1.19.3/libexec/src/runtime/panic.go:890 +0x262
github.com/smallstep/certificates/api/log.Error({0x2b69e288?, 0xc00003fcc0?}, {0x261e1e0?, 0xc000113020?})
	/private/tmp/step--certificates-20221114-4484-11q7obf/api/log/log.go:44 +0x176
github.com/smallstep/certificates/api/render.Error({0x2b69e288, 0xc00003fcc0}, {0x261e1e0?, 0xc000113020?})
	/private/tmp/step--certificates-20221114-4484-11q7obf/api/render/render.go:79 +0x3b
github.com/smallstep/certificates/acme/api.GetChallenge({0x2b69e288, 0xc00003fcc0}, 0xc00081e100)
	/private/tmp/step--certificates-20221114-4484-11q7obf/acme/api/handler.go:354 +0x711
github.com/smallstep/certificates/acme/api.verifyAndExtractJWSPayload.func1({0x2b69e288, 0xc00003fcc0}, 0xc00081e000)
	/private/tmp/step--certificates-20221114-4484-11q7obf/acme/api/middleware.go:400 +0x39a
github.com/smallstep/certificates/acme/api.lookupJWK.func1({0x2b69e288, 0xc00003fcc0}, 0xc005c9af00)
	/private/tmp/step--certificates-20221114-4484-11q7obf/acme/api/middleware.go:329 +0x407
github.com/smallstep/certificates/acme/api.validateJWS.func1({0x2b69e288, 0xc00003fcc0}, 0xc005c9af00)
	/private/tmp/step--certificates-20221114-4484-11q7obf/acme/api/middleware.go:210 +0x770
github.com/smallstep/certificates/acme/api.parseJWS.func1({0x2b69e288, 0xc00003fcc0}, 0xc005c9ae00)
	/private/tmp/step--certificates-20221114-4484-11q7obf/acme/api/middleware.go:110 +0x1a4
github.com/smallstep/certificates/acme/api.verifyContentType.func1({0x2b69e288, 0xc00003fcc0}, 0xc005c9ae00)
	/private/tmp/step--certificates-20221114-4484-11q7obf/acme/api/middleware.go:87 +0x336
github.com/smallstep/certificates/acme/api.addDirLink.func1({0x2b69e288, 0xc00003fcc0}, 0xc005c9ae00)
	/private/tmp/step--certificates-20221114-4484-11q7obf/acme/api/middleware.go:57 +0x22e
github.com/smallstep/certificates/acme/api.addNonce.func1({0x2b69e288, 0xc00003fcc0}, 0xc005c9ae00)
	/private/tmp/step--certificates-20221114-4484-11q7obf/acme/api/middleware.go:45 +0x24a
github.com/smallstep/certificates/acme/api.checkPrerequisites.func1({0x2b69e288, 0xc00003fcc0}, 0xc005c9ae00)
	/private/tmp/step--certificates-20221114-4484-11q7obf/acme/api/middleware.go:285 +0xcb
net/http.HandlerFunc.ServeHTTP(0x262ba98?, {0x2b69e288?, 0xc00003fcc0?}, 0x309d510?)
	/usr/local/Cellar/go/1.19.3/libexec/src/net/http/server.go:2109 +0x2f
github.com/smallstep/certificates/acme.(*linker).Middleware.func1({0x2b69e288, 0xc00003fcc0}, 0xc005c9ac00)
	/private/tmp/step--certificates-20221114-4484-11q7obf/acme/linker.go:203 +0x21b
net/http.HandlerFunc.ServeHTTP(0xc0002b2ec0?, {0x2b69e288?, 0xc00003fcc0?}, 0x1?)
	/usr/local/Cellar/go/1.19.3/libexec/src/net/http/server.go:2109 +0x2f
github.com/smallstep/certificates/acme/api.route.func1.1({0x2b69e288, 0xc00003fcc0}, 0xc005c9ac00)
	/private/tmp/step--certificates-20221114-4484-11q7obf/acme/api/handler.go:128 +0xae
net/http.HandlerFunc.ServeHTTP(0x204ce00?, {0x2b69e288?, 0xc00003fcc0?}, 0xc000094780?)
	/usr/local/Cellar/go/1.19.3/libexec/src/net/http/server.go:2109 +0x2f
github.com/go-chi/chi.(*Mux).routeHTTP(0xc000051980, {0x2b69e288, 0xc00003fcc0}, 0xc005c9ac00)
	/Users/brew/Library/Caches/Homebrew/go_mod_cache/pkg/mod/github.com/go-chi/[email protected]+incompatible/mux.go:431 +0x1f9
net/http.HandlerFunc.ServeHTTP(0xc005cf9140?, {0x2b69e288?, 0xc00003fcc0?}, 0xc000177728?)
	/usr/local/Cellar/go/1.19.3/libexec/src/net/http/server.go:2109 +0x2f
github.com/go-chi/chi.(*Mux).ServeHTTP(0xc000051980, {0x2b69e288, 0xc00003fcc0}, 0xc005c9ac00)
	/Users/brew/Library/Caches/Homebrew/go_mod_cache/pkg/mod/github.com/go-chi/[email protected]+incompatible/mux.go:70 +0x2dc
github.com/go-chi/chi.(*Mux).Mount.func1({0x2b69e288, 0xc00003fcc0}, 0xc005c9ac00)
	/Users/brew/Library/Caches/Homebrew/go_mod_cache/pkg/mod/github.com/go-chi/[email protected]+incompatible/mux.go:298 +0x13b
net/http.HandlerFunc.ServeHTTP(0x204ce00?, {0x2b69e288?, 0xc00003fcc0?}, 0xc0007a4e75?)
	/usr/local/Cellar/go/1.19.3/libexec/src/net/http/server.go:2109 +0x2f
github.com/go-chi/chi.(*Mux).routeHTTP(0xc000050f00, {0x2b69e288, 0xc00003fcc0}, 0xc005c9ac00)
	/Users/brew/Library/Caches/Homebrew/go_mod_cache/pkg/mod/github.com/go-chi/[email protected]+incompatible/mux.go:431 +0x1f9
net/http.HandlerFunc.ServeHTTP(0xc005cf9140?, {0x2b69e288?, 0xc00003fcc0?}, 0x2b4657a0?)
	/usr/local/Cellar/go/1.19.3/libexec/src/net/http/server.go:2109 +0x2f
github.com/go-chi/chi/middleware.GetHead.func1({0x2b69e288?, 0xc00003fcc0?}, 0x303dd01?)
	/Users/brew/Library/Caches/Homebrew/go_mod_cache/pkg/mod/github.com/go-chi/[email protected]+incompatible/middleware/get_head.go:37 +0x1ee
net/http.HandlerFunc.ServeHTTP(0x262ba98?, {0x2b69e288?, 0xc00003fcc0?}, 0x303dd10?)
	/usr/local/Cellar/go/1.19.3/libexec/src/net/http/server.go:2109 +0x2f
github.com/go-chi/chi.(*Mux).ServeHTTP(0xc000050f00, {0x2b69e288, 0xc00003fcc0}, 0xc005c9ab00)
	/Users/brew/Library/Caches/Homebrew/go_mod_cache/pkg/mod/github.com/go-chi/[email protected]+incompatible/mux.go:86 +0x296
github.com/smallstep/certificates/logging.(*LoggerHandler).ServeHTTP(0xc00072bda0, {0x262a3f8, 0xc00059c2a0}, 0x2617e40?)
	/private/tmp/step--certificates-20221114-4484-11q7obf/logging/handler.go:50 +0xa3
github.com/smallstep/certificates/logging.RequestID.func1.1({0x262a3f8, 0xc00059c2a0}, 0xc005c9aa00)
	/private/tmp/step--certificates-20221114-4484-11q7obf/logging/context.go:38 +0x2b0
net/http.HandlerFunc.ServeHTTP(0x0?, {0x262a3f8?, 0xc00059c2a0?}, 0x12f6574?)
	/usr/local/Cellar/go/1.19.3/libexec/src/net/http/server.go:2109 +0x2f
net/http.serverHandler.ServeHTTP({0x2626458?}, {0x262a3f8, 0xc00059c2a0}, 0xc005c9aa00)
	/usr/local/Cellar/go/1.19.3/libexec/src/net/http/server.go:2947 +0x30c
net/http.(*conn).serve(0xc0008ac1e0, {0x262ba98, 0xc0007380c0})
	/usr/local/Cellar/go/1.19.3/libexec/src/net/http/server.go:1991 +0x607
created by net/http.(*Server).Serve
	/usr/local/Cellar/go/1.19.3/libexec/src/net/http/server.go:3102 +0x4db

taher-mosbah avatar Nov 17 '22 09:11 taher-mosbah

@taher-mosbah it seems the challenge cannot be validated and returns an error. Then in handling the error, it hits a nil pointer when trying to format the stacktrace, so the reason it failed in the first place is lost 😓 I think I know a fix for the latter, but I'll need to test it quick with an iOS device.

Edit: I have reproduced the nil pointer error. It'll only happen with STEPDEBUG=1, but it's sensible to use that in this situation 😅 Now looking into the root cause.

hslatman avatar Nov 17 '22 09:11 hslatman

@taher-mosbah can you retry, but without STEPDEBUG=1? It's a temporary workaround for the bug. I don't think you'll lose information. I'm fairly certain the error you'll get looks like this:

INFO[0008] ... response="{\"type\":\"device-attest-01\",\"status\":\"invalid\",\"token\":\"uYsGs6r...oWDji\",\"url\":\"https://192.168.0.190:6443/acme/acme/challenge/qreyHm...mo2HY/N9JdkRfp...yVleiuikh5\",\"error\":{\"type\":\"urn:ietf:params:acme:error:badAttestationStatement\",\"detail\":\"Attestation statement cannot be verified\"}}" size=333 status=200 user-agent=com.apple.security.acmeclient/1.0 user-id=

I trigged this case myself by having a .mobileconfig that contained the wrong ClientIdentifier for my iOS device, but there are several checks that can be hit. With all prerequisites in place, I got my iOS device to get a certificate successfully.

hslatman avatar Nov 17 '22 10:11 hslatman

retried with STEPDEBUG=0 and the error I have seems different from you:

{"duration":"2.012958ms","duration-ns":2012958,"error":"error validating challenge: error unmarshalling CBOR: EOF","fields.time":"2022-11-17T10:31:13Z","level":"error","method":"POST","msg":"","name":"ca","nonce":"cUpobUVOQmVNWjBkemg2aXpJcEoyeWRFRk9MVUQxRG4","path":"/acme/acme2/challenge/frqKeYRfhY9nBl73EXZVVdbC2iozathd/bFn0SD9zfWJw1Ajw5LvGtkUElOp0W55H","protocol":"HTTP/1.1","referer":"","remote-address":"fe80::58d8:12ff:fe1b:9054%en9","request-id":"cdr0qsbr1m301s697jhg","response":"{\"type\":\"urn:ietf:params:acme:error:serverInternal\",\"detail\":\"The server experienced an internal error\"}","size":105,"status":500,"time":"2022-11-17T10:31:13Z","user-agent":"com.apple.security.acmeclient/1.0","user-id":""}

By the way I'm using my iPhone UDID as ClientIdentifier is this the expected ID ?

taher-mosbah avatar Nov 17 '22 10:11 taher-mosbah

Can you share your (redacted!).mobileconfig, or the settings used to create it? It seems the attestation evidence isn't provided or cannot be parsed correctly. Maybe that's due to iOS not sending it due to an error in the profile.

We're indeed checking for the UDID to be equal to the ClientIdentifier.

hslatman avatar Nov 17 '22 10:11 hslatman

Thanks @hslatman I managed to get it to work ! I was testing on macOS and forgot to HardwareBound back to true.

taher-mosbah avatar Nov 17 '22 11:11 taher-mosbah

Great to hear, @taher-mosbah 😄

Thank you for pointing out the issue with the stack trace logging; not a fun one 😅 A fix is underway: https://github.com/smallstep/certificates/pull/1187.

I'll have a look at improving our error handling and logging around device-attest-01, to make it easier to debug potential issues when trying it out. Tagging @tashian as this may be something to include something about in the use case docs (i.e. ensuring the .mobileconfig has the right properties set)

hslatman avatar Nov 17 '22 13:11 hslatman

I don't really understand the IETF process, but it seems the draft linked in the main issue body has moved forward to https://datatracker.ietf.org/doc/draft-acme-device-attest/

benlongo avatar Feb 07 '23 21:02 benlongo

I'm closing this, because we have full support for the current state of the IETF draft for device-attest-01. We'll be iterating on some implementation details, validation logic and debugging options.

New issues can be opened in the future 🙂

hslatman avatar May 02 '23 13:05 hslatman